/*-------------------------------------------------------------------------
 *
 * xlogrecovery.c
 *		WAL 恢复、备用模式的函数
 *
 * 该源文件包含控制 WAL 恢复的函数。
 * InitWalRecovery() 根据配置选项和控制文件的状态，初始化系统以进行崩溃或
 * 归档恢复或备用模式。PerformWalRecovery() 执行实际的 WAL 重播，调用 rmgr 特定的重做例程。
 * EndWalRecovery() 执行恢复结束检查和清理操作，并准备初始化 WAL 以进行写入的所需信息。
 * 除这三大主要功能外，还有一系列函数用于查询恢复状态和控制恢复过程。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/xlogrecovery.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <ctype.h>
#include <math.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

#include "access/timeline.h"
#include "access/transam.h"
#include "access/xact.h"
#include "access/xlog_internal.h"
#include "access/xlogarchive.h"
#include "access/xlogprefetcher.h"
#include "access/xlogreader.h"
#include "access/xlogrecovery.h"
#include "access/xlogutils.h"
#include "backup/basebackup.h"
#include "catalog/pg_control.h"
#include "commands/tablespace.h"
#include "common/file_utils.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/bgwriter.h"
#include "postmaster/startup.h"
#include "replication/walreceiver.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/latch.h"
#include "storage/pmsignal.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/spin.h"
#include "utils/builtins.h"
#include "utils/guc.h"
#include "utils/ps_status.h"
#include "utils/pg_rusage.h"

/* 不支持的旧恢复命令文件名（相对于 $PGDATA） */
#define RECOVERY_COMMAND_FILE	"recovery.conf"
#define RECOVERY_COMMAND_DONE	"recovery.done"

/*
 * GUC 支持
 */
const struct config_enum_entry recovery_target_action_options[] = {
	{"pause", RECOVERY_TARGET_ACTION_PAUSE, false},
	{"promote", RECOVERY_TARGET_ACTION_PROMOTE, false},
	{"shutdown", RECOVERY_TARGET_ACTION_SHUTDOWN, false},
	{NULL, 0, false}
};

/* 从 recovery.conf 中获取的用于归档恢复的选项 */
char	   *recoveryRestoreCommand = NULL;
char	   *recoveryEndCommand = NULL;
char	   *archiveCleanupCommand = NULL;
RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
bool		recoveryTargetInclusive = true;
int			recoveryTargetAction = RECOVERY_TARGET_ACTION_PAUSE;
TransactionId recoveryTargetXid;
char	   *recovery_target_time_string;
TimestampTz recoveryTargetTime;
const char *recoveryTargetName;
XLogRecPtr	recoveryTargetLSN;
int			recovery_min_apply_delay = 0;

/* 从 recovery.conf 中获取的用于 XLOG 流的选项 */
char	   *PrimaryConnInfo = NULL;
char	   *PrimarySlotName = NULL;
char	   *PromoteTriggerFile = NULL;
bool		wal_receiver_create_temp_slot = false;

/*
 * recoveryTargetTimeLineGoal: 用户请求的目标，如果有的话
 *
 * recoveryTargetTLIRequested: 请求的时间线的数值，如果是常量
 *
 * recoveryTargetTLI: 当前理解的目标时间线；可能会发生变化
 *
 * expectedTLEs: recoveryTargetTLI 及其已知父时间线的 TimeLineHistoryEntries 列表，
 * 最新的在前（所以 recoveryTargetTLI 始终是第一个列表成员）。 只有这些 TLIs 被预期在
 * 我们读取的 WAL 段中看到，并且确实只有这些 TLIs 会被视为候选 WAL 文件以便打开。
 *
 * curFileTLI: 当前输入 WAL 文件名称中出现的 TLI。
 * （这不一定是我们正在重放 WAL 的时间线，StartupXLOG 称为 replayTLI，因为我们可能在
 * 扫描数据，这些数据来自于当前文件创建时的祖先时间线。）在顺序扫描中不允许该值
 * 下降。
 */
RecoveryTargetTimeLineGoal recoveryTargetTimeLineGoal = RECOVERY_TARGET_TIMELINE_LATEST;
TimeLineID	recoveryTargetTLIRequested = 0;
TimeLineID	recoveryTargetTLI = 0;
static List *expectedTLEs;
static TimeLineID curFileTLI;

/*
 * 当 ArchiveRecoveryRequested 被设置时，请求了归档恢复，
 * 即存在信号文件。当 InArchiveRecovery 被设置时，我们正在
 * 使用离线 XLOG 归档进行恢复。这些变量仅在启动过程中有效。
 *
 * 当 ArchiveRecoveryRequested 为真，但 InArchiveRecovery 为假时，我们
 * 正在仅使用 pg_wal 中的 XLOG 文件进行崩溃恢复，但
 * 一旦我们到达 pg_wal 中 WAL 的末尾，将切换到使用离线 XLOG 归档。
*/
bool		ArchiveRecoveryRequested = false;
bool		InArchiveRecovery = false;

/*
 * 当 StandbyModeRequested 被设置时，请求了备用模式，即
 * standby.signal 文件存在。当 StandbyMode 被设置时，我们当前
 * 处于备用模式。这些变量仅在启动过程中有效。
 * 它们的作用类似于 ArchiveRecoveryRequested 和 InArchiveRecovery。
 */
static bool StandbyModeRequested = false;
bool		StandbyMode = false;

/* 启动时是否存在信号文件？ */
static bool standby_signal_file_found = false;
static bool recovery_signal_file_found = false;

/*
 * CheckPointLoc 是决定从哪个位置开始重放的检查点记录位置。
 * 它来自备份标签文件或控制文件。
 *
 * RedoStartLSN 是检查点的 REDO 位置，也来自备份标签
 * 文件或控制文件。在备用模式下，XLOG 流通常从发现无效记录的位置
 * 开始。但是如果我们甚至无法读取初始检查点记录，则使用 REDO 位置
 * 而不是检查点位置作为 XLOG 流的起始位置。
 * 否则，在读取检查点记录后，我们必须跳回到 REDO 位置，
 * 因为 REDO 记录可能先于检查点记录。
 */
static XLogRecPtr CheckPointLoc = InvalidXLogRecPtr;
static TimeLineID CheckPointTLI = 0;
static XLogRecPtr RedoStartLSN = InvalidXLogRecPtr;
static TimeLineID RedoStartTLI = 0;

/*
 * SharedHotStandbyActive 变量的本地副本。 False 实际上意味着“未知，需要检查共享状态”。
 */
static bool LocalHotStandbyActive = false;

/*
 * SharedPromoteIsTriggered变量的本地副本。False实际上意味着“未知，需要检查共享状态”。
 */
static bool LocalPromoteIsTriggered = false;

/* 恢复代码是否请求了walreceiver唤醒？ */
static bool doRequestWalReceiverReply;

/* XLogReader对象用于解析WAL记录 */
static XLogReaderState *xlogreader = NULL;

/* XLogPrefetcher对象用于预读WAL记录 */
static XLogPrefetcher *xlogprefetcher = NULL;

/* 从ReadRecord传递到XLogPageRead回调的参数。 */
typedef struct XLogPageReadPrivate
{
	int			emode;
	bool		fetching_ckpt;	/* 我们是否正在获取检查点记录？ */
	bool		randAccess;
	TimeLineID	replayTLI;
} XLogPageReadPrivate;

/* 告诉XLogPageRead我们已经开始重放的标志 */
static bool InRedo = false;

/*
 * 指示在恢复期间我们从哪里获取WAL文件的代码，或者尝试从哪里获取一个。
 */
typedef enum
{
	XLOG_FROM_ANY = 0,			/* 请求从任何源读取WAL */
	XLOG_FROM_ARCHIVE,			/* 使用restore_command恢复 */
	XLOG_FROM_PG_WAL,			/* 在pg_wal中现有的文件 */
	XLOG_FROM_STREAM			/* 从主服务器流式传输 */
} XLogSource;

/* 用于调试输出的XLogSources的人类可读名称 */
static const char *const xlogSourceNames[] = {"any", "archive", "pg_wal", "stream"};

/*
 * readFile是-1或当前打开进行读取的日志文件段的内核FD。readSegNo识别段。readOff是刚读取的页面的偏移量，readLen指示已读取到readBuf的长度，readSource指示我们从哪里获取了当前打开的文件。
 *
 * 注意：我们也可以使用Reserve/ReleaseExternalFD来跟踪这个FD的消费（就像在xlog.c中的openLogFile）；但目前似乎没有必要，因为XLOG并不是由通用会话读取的。
 */
static int	readFile = -1;
static XLogSegNo readSegNo = 0;
static uint32 readOff = 0;
static uint32 readLen = 0;
static XLogSource readSource = XLOG_FROM_ANY;

/*
 * 跟踪我们当前正在读取的源。这与readSource不同，因为即使我们当前没有打开WAL文件，这也是始终设置的。如果lastSourceFailed被设置，我们最后从currentSource读取的尝试失败，我们应该下次尝试另一个源。
 *
 * 当发生配置更改需要walreceiver重启时，pendingWalRcvRestart被设置。这仅在XLOG_FROM_STREAM状态下有效。
 */
static XLogSource currentSource = XLOG_FROM_ANY;
static bool lastSourceFailed = false;
static bool pendingWalRcvRestart = false;

/*
 * 这些变量跟踪我们上次获取一些WAL数据以进行处理的时间，以及我们从哪里获取它。（XLogReceiptSource最初与readSource相同，但当我们没有数据可处理时，readSource会重置为零。它与currentSource也不同，当我们尝试从源读取并失败时，currentSource也会发生变化，而XLogReceiptSource跟踪我们上次成功读取的WAL的位置。）
 */
static TimestampTz XLogReceiptTime = 0;
static XLogSource XLogReceiptSource = XLOG_FROM_ANY;

/* WalRcv->flushedUpto的本地副本 */
static XLogRecPtr flushedUpto = 0;
static TimeLineID receiveTLI = 0;

/*
 * 从控制文件中复制minRecoveryPoint和backupEndPoint。
 *
 * 为了达到一致性，我们必须将WAL重放到minRecoveryPoint。如果backupEndRequired为真，我们还必须达到backupEndPoint，或者如果它无效，达到对应于backupStartPoint的备份结束记录。
 *
 * 注意：在归档恢复中，在达到一致性后，xlog.c中的函数将开始更新控制文件中的minRecoveryPoint。但这个minRecoveryPoint变量的副本反映了恢复开始时的值，并且在达到一致性后不会更新。
 */
static XLogRecPtr minRecoveryPoint;
static TimeLineID minRecoveryPointTLI;

static XLogRecPtr backupStartPoint;
static XLogRecPtr backupEndPoint;
static bool backupEndRequired = false;

/*
 * 我们是否达到了一个一致的数据库状态？在崩溃恢复中，我们必须重新播放所有的WAL，因此reachedConsistency从未设置。在归档恢复期间，一旦达到minRecoveryPoint，数据库就一致了。
 *
 * 一致的状态意味着系统内部一致，所有的WAL都已播放到某个点，并且重要的是，磁盘上没有后续操作的痕迹。
 */
bool		reachedConsistency = false;

/* 专门用于大小为BLCKSZ的一致性检查的缓冲区 */
static char *replay_image_masked = NULL;
static char *primary_image_masked = NULL;


/*
 * WAL恢复的共享内存状态。
 */
typedef struct XLogRecoveryCtlData
{
	/*
	 * SharedHotStandbyActive指示我们是否允许执行热备份查询。
	 * 由info_lck保护。
	 */
	bool		SharedHotStandbyActive;

	/*
	 * SharedPromoteIsTriggered指示备用机的提升是否已被触发。
	 * 由info_lck保护。
	 */
	bool		SharedPromoteIsTriggered;

	/*
	 * recoveryWakeupLatch用于唤醒启动进程以继续WAL重放，
	 * 如果它正在等待WAL到达或故障切换触发文件出现。
	 *
	 * 请注意，启动进程还使用另一个闩锁，即procLatch，
	 * 等待恢复冲突。如果我们放弃使用recoveryWakeupLatch来
	 * 发信号给启动进程，而改用procLatch，这样更符合可能使用
	 * 该闩锁的通用信号处理程序的情况。但我们不应该这样做，因为
	 * 启动进程并不假设它是被walreceiver进程或SIGHUP信号处理程序
	 * 唤醒的，同时它在等待恢复冲突。分别使用recoveryWakeupLatch和
	 * procLatch进行WAL重放和恢复冲突的进程间通信。
	 */
	Latch		recoveryWakeupLatch;

	/*
	 * 最后成功重放的记录。
	 */
	XLogRecPtr	lastReplayedReadRecPtr; /* 开始位置 */
	XLogRecPtr	lastReplayedEndRecPtr;	/* 结束+1位置 */
	TimeLineID	lastReplayedTLI;	/* 时间线 */

	/*
	 * 当我们当前正在重放一条记录，即在redo函数中时，
	 * replayEndRecPtr指向正在重放的记录的结束+1，
	 * 否则它等于lastReplayedEndRecPtr。
	 */
	XLogRecPtr	replayEndRecPtr;
	TimeLineID	replayEndTLI;
	/* 最后重放（或正在重放）的COMMIT/ABORT记录的时间戳 */
	TimestampTz recoveryLastXTime;

	/*
	 * 我们开始重放当前WAL数据块的时间戳，
	 * 仅与复制或归档恢复相关
	 */
	TimestampTz currentChunkStartTime;
	/* 恢复暂停状态 */
	RecoveryPauseState recoveryPauseState;
	ConditionVariable recoveryNotPausedCV;

	slock_t		info_lck;		/* 锁定上述共享变量 */
} XLogRecoveryCtlData;

static XLogRecoveryCtlData *XLogRecoveryCtl = NULL;

/*
 * abortedRecPtr是WAL结束时损坏记录的开始指针，
 * 当恢复完成时；missingContrecPtr是第一个
 * 丢失的contrecord的位置。详情见CreateOverwriteContrecordRecord。
 */
static XLogRecPtr abortedRecPtr;
static XLogRecPtr missingContrecPtr;

/*
 * 如果recoveryStopsBefore/After返回true，它在此处保存停止点的信息
 */
static TransactionId recoveryStopXid;
static TimestampTz recoveryStopTime;
static XLogRecPtr recoveryStopLSN;
static char recoveryStopName[MAXFNAMELEN];
static bool recoveryStopAfter;

/* 本地函数的原型 */
static void fc_ApplyWalRecord(XLogReaderState *fc_xlogreader, XLogRecord *fc_record, TimeLineID *fc_replayTLI);

static void fc_EnableStandbyMode(void);
static void fc_readRecoverySignalFile(void);
static void fc_validateRecoveryParameters(void);
static bool fc_read_backup_label(XLogRecPtr *fc_checkPointLoc,
							  TimeLineID *fc_backupLabelTLI,
							  bool *fc_backupEndRequired, bool *fc_backupFromStandby);
static bool fc_read_tablespace_map(List **fc_tablespaces);

static void fc_xlogrecovery_redo(XLogReaderState *fc_record, TimeLineID fc_replayTLI);
static void fc_CheckRecoveryConsistency(void);
static void fc_rm_redo_error_callback(void *fc_arg);
#ifdef WAL_DEBUG
static void fc_xlog_outrec(StringInfo fc_buf, XLogReaderState *fc_record);
#endif
static void fc_xlog_block_info(StringInfo fc_buf, XLogReaderState *fc_record);
static void fc_checkTimeLineSwitch(XLogRecPtr fc_lsn, TimeLineID fc_newTLI,
								TimeLineID fc_prevTLI, TimeLineID fc_replayTLI);
static bool fc_getRecordTimestamp(XLogReaderState *fc_record, TimestampTz *fc_recordXtime);
static void fc_verifyBackupPageConsistency(XLogReaderState *fc_record);

static bool fc_recoveryStopsBefore(XLogReaderState *fc_record);
static bool fc_recoveryStopsAfter(XLogReaderState *fc_record);
static char *fc_getRecoveryStopReason(void);
static void fc_recoveryPausesHere(bool fc_endOfRecovery);
static bool fc_recoveryApplyDelay(XLogReaderState *fc_record);
static void fc_ConfirmRecoveryPaused(void);

static XLogRecord *fc_ReadRecord(XLogPrefetcher *fc_xlogprefetcher,
							  int fc_emode, bool fc_fetching_ckpt,
							  TimeLineID fc_replayTLI);

static int	fc_XLogPageRead(XLogReaderState *fc_xlogreader, XLogRecPtr fc_targetPagePtr,
						 int fc_reqLen, XLogRecPtr fc_targetRecPtr, char *fc_readBuf);
static XLogPageReadResult fc_WaitForWALToBecomeAvailable(XLogRecPtr fc_RecPtr,
													  bool fc_randAccess,
													  bool fc_fetching_ckpt,
													  XLogRecPtr fc_tliRecPtr,
													  TimeLineID fc_replayTLI,
													  XLogRecPtr fc_replayLSN,
													  bool fc_nonblocking);
static int	fc_emode_for_corrupt_record(int fc_emode, XLogRecPtr fc_RecPtr);
static XLogRecord *fc_ReadCheckpointRecord(XLogPrefetcher *fc_xlogprefetcher, XLogRecPtr fc_RecPtr,
										int fc_whichChkpt, bool fc_report, TimeLineID fc_replayTLI);
static bool fc_rescanLatestTimeLine(TimeLineID fc_replayTLI, XLogRecPtr fc_replayLSN);
static int	fc_XLogFileRead(XLogSegNo fc_segno, int fc_emode, TimeLineID fc_tli,
						 XLogSource fc_source, bool fc_notfoundOk);
static int	fc_XLogFileReadAnyTLI(XLogSegNo fc_segno, int fc_emode, XLogSource fc_source);

static bool fc_CheckForStandbyTrigger(void);
static void fc_SetPromoteIsTriggered(void);
static bool fc_HotStandbyActiveInReplay(void);

static void fc_SetCurrentChunkStartTime(TimestampTz fc_xtime);
static void fc_SetLatestXTime(TimestampTz fc_xtime);

/*
 * 为WAL恢复初始化共享内存
 */
Size XLogRecoveryShmemSize(void)
{
	Size		fc_size;

	/* XLogRecoveryCtl */
	fc_size = sizeof(XLogRecoveryCtlData);

	return fc_size;
}

void XLogRecoveryShmemInit(void)
{
	bool		fc_found;

	XLogRecoveryCtl = (XLogRecoveryCtlData *)
		ShmemInitStruct("XLOG Recovery Ctl", XLogRecoveryShmemSize(), &fc_found);
	if (fc_found)
		return;
	memset(XLogRecoveryCtl, 0, sizeof(XLogRecoveryCtlData));

	SpinLockInit(&XLogRecoveryCtl->info_lck);
	InitSharedLatch(&XLogRecoveryCtl->recoveryWakeupLatch);
	ConditionVariableInit(&XLogRecoveryCtl->recoveryNotPausedCV);
}

/*
 * 一个薄薄的包装器，用于启用StandbyMode并根据需要进行其他准备工作。
 */
static void fc_EnableStandbyMode(void)
{
	StandbyMode = true;

	/*
	 * 为了避免服务器日志膨胀，我们不报告备用节点的恢复进度
	 * 因为它将始终处于恢复状态，除非被提升。我们在备用模式下禁用
	 * 启动进度超时，以避免不必要地调用
	 * startup_progress_timeout_handler()。
	 */
	disable_startup_progress_timeout();
}

/*
 * 如有需要，为WAL恢复做好系统准备。
 *
 * 这个函数由StartupXLOG()调用，后者协调服务器启动
 * 序列。这个函数分析控制文件和备份标签
 * 文件（如果有的话），并确定我们是否需要进行崩溃恢复或
 * 存档恢复，以及我们需要回放WAL多远才能达到
 * 一致状态。
 *
 * 这还不会改变磁盘上的状态，除非创建表空间映射文件的符号链接（如果有的话），以及获取
 * 查找检查点记录所需的WAL文件。在入口时，调用者已经将控制
 * 文件读入内存，并作为参数传递。这个函数更新它以
 * 反映恢复状态，而调用者预计在初始化其他子系统后将其写回到
 * 磁盘，但在调用PerformWalRecovery()之前。
 *
 * 这会初始化一些全局变量，如ArchiveModeRequested，和
 * StandbyModeRequested以及InRecovery。
 */
void InitWalRecovery(ControlFileData *fc_ControlFile, bool *fc_wasShutdown_ptr,
				bool *fc_haveBackupLabel_ptr, bool *fc_haveTblspcMap_ptr)
{
	XLogPageReadPrivate *fc_private;
	struct stat fc_st;
	bool		fc_wasShutdown;
	XLogRecord *fc_record;
	DBState		fc_dbstate_at_startup;
	bool		fc_haveTblspcMap = false;
	bool		fc_haveBackupLabel = false;
	CheckPoint	fc_checkPoint;
	bool		fc_backupFromStandby = false;

	fc_dbstate_at_startup = fc_ControlFile->state;

	/*
	 * 初始化时假设我们想要恢复到最新活动的时间线，
	 * 根据pg_control。
	 */
	if (fc_ControlFile->minRecoveryPointTLI >
		fc_ControlFile->checkPointCopy.ThisTimeLineID)
		recoveryTargetTLI = fc_ControlFile->minRecoveryPointTLI;
	else
		recoveryTargetTLI = fc_ControlFile->checkPointCopy.ThisTimeLineID;

	/*
	 * 检查信号文件，如果存在则设置离线恢复状态
	 */
	fc_readRecoverySignalFile();
	fc_validateRecoveryParameters();

	if (ArchiveRecoveryRequested)
	{
		if (StandbyModeRequested)
			ereport(LOG,
					(errmsg("entering standby mode")));
		else if (recoveryTarget == RECOVERY_TARGET_XID)
			ereport(LOG,
					(errmsg("starting point-in-time recovery to XID %u",
							recoveryTargetXid)));
		else if (recoveryTarget == RECOVERY_TARGET_TIME)
			ereport(LOG,
					(errmsg("starting point-in-time recovery to %s",
							timestamptz_to_str(recoveryTargetTime))));
		else if (recoveryTarget == RECOVERY_TARGET_NAME)
			ereport(LOG,
					(errmsg("starting point-in-time recovery to \"%s\"",
							recoveryTargetName)));
		else if (recoveryTarget == RECOVERY_TARGET_LSN)
			ereport(LOG,
					(errmsg("starting point-in-time recovery to WAL location (LSN) \"%X/%X\"",
							LSN_FORMAT_ARGS(recoveryTargetLSN))));
		else if (recoveryTarget == RECOVERY_TARGET_IMMEDIATE)
			ereport(LOG,
					(errmsg("starting point-in-time recovery to earliest consistent point")));
		else
			ereport(LOG,
					(errmsg("starting archive recovery")));
	}

	/*
	 * 如果我们在恢复期间要进入休眠，则接管唤醒信号。
	 */
	if (ArchiveRecoveryRequested)
		OwnLatch(&XLogRecoveryCtl->recoveryWakeupLatch);

	fc_private = palloc0(sizeof(XLogPageReadPrivate));
	xlogreader =
		XLogReaderAllocate(wal_segment_size, NULL,
						   XL_ROUTINE(.page_read = &fc_XLogPageRead,
									  .segment_open = NULL,
									  .segment_close = wal_segment_close),
						   fc_private);
	if (!xlogreader)
		ereport(ERROR,
				(errcode(ERRCODE_OUT_OF_MEMORY),
				 errmsg("out of memory"),
				 errdetail("Failed while allocating a WAL reading processor.")));
	xlogreader->system_identifier = fc_ControlFile->system_identifier;

	/*
	 * 设置WAL解码缓冲区大小。这限制了我们可以在
	 * WAL中提前读取的范围。
	 */
	XLogReaderSetDecodeBuffer(xlogreader, NULL, wal_decode_buffer_size);

	/* 创建WAL预取器。 */
	xlogprefetcher = XLogPrefetcherAllocate(xlogreader);

	/*
	 * 分配两个专用于WAL一致性检查的页面缓冲区。我们这样做，而不仅仅是制作静态数组，原因有两个：
	 * （1）在大多数后端实例化中不需要浪费存储空间；
	 * （2）静态字符数组不能保证具有任何特定的
	 * 对齐方式，而palloc()将提供MAXALIGN的存储。
	 */
	replay_image_masked = (char *) palloc(BLCKSZ);
	primary_image_masked = (char *) palloc(BLCKSZ);

	if (fc_read_backup_label(&CheckPointLoc, &CheckPointTLI, &backupEndRequired,
						  &fc_backupFromStandby))
	{
		List	   *fc_tablespaces = NIL;

		/*
		 * 请求了存档恢复，感谢备份标签
		 * 文件，我们知道我们需要回放多远才能达到一致性。直接进入
		 * 存档恢复。
		 */
		InArchiveRecovery = true;
		if (StandbyModeRequested)
			fc_EnableStandbyMode();

		/*
		 * 在创建新的副本、PITR节点等时省略backup_label
		 * 不幸的是是导致损坏的一个常见原因。记录
		 * 使用了backup_label使得排除这一点作为
		 * 观察到的损坏原因变得稍微容易一些。
		 *
		 * 在尝试读取检查点记录（可能会失败）之前这样做，
		 * 否则很难理解为什么使用的检查点是ControlFile->checkPoint以外的其他。
		 */
		ereport(LOG,
				(errmsg("starting backup recovery with redo LSN %X/%X, checkpoint LSN %X/%X, on timeline ID %u",
						LSN_FORMAT_ARGS(RedoStartLSN),
						LSN_FORMAT_ARGS(CheckPointLoc),
						CheckPointTLI)));

		/*
		 * 当存在backup_label文件时，我们希望从
		 * 它识别的检查点向前滚动，而不是使用pg_control。
		 */
		fc_record = fc_ReadCheckpointRecord(xlogprefetcher, CheckPointLoc, 0, true,
									  CheckPointTLI);
		if (fc_record != NULL)
		{
			memcpy(&fc_checkPoint, XLogRecGetData(xlogreader), sizeof(CheckPoint));
			fc_wasShutdown = ((fc_record->xl_info & ~XLR_INFO_MASK) == XLOG_CHECKPOINT_SHUTDOWN);
			ereport(DEBUG1,
					(errmsg_internal("checkpoint record is at %X/%X",
									 LSN_FORMAT_ARGS(CheckPointLoc))));
			InRecovery = true;	/* 即使SHUTDOWNED也强制恢复 */

			/*
			 * 确保 REDO 位置存在。如果在在线备份期间发生崩溃，可能会出现这种情况，
			 * 此时会留下一个 backup_label，指向一个已经归档的 WAL 段。
			 */
			if (fc_checkPoint.redo < CheckPointLoc)
			{
				XLogPrefetcherBeginRead(xlogprefetcher, fc_checkPoint.redo);
				if (!fc_ReadRecord(xlogprefetcher, LOG, false,
								fc_checkPoint.ThisTimeLineID))
					ereport(FATAL,
							(errmsg("could not find redo location referenced by checkpoint record"),
							 errhint("If you are restoring from a backup, touch \"%s/recovery.signal\" and add required recovery options.\n"
									 "If you are not restoring from a backup, try removing the file \"%s/backup_label\".\n"
									 "Be careful: removing \"%s/backup_label\" will result in a corrupt cluster if restoring from a backup.",
									 DataDir, DataDir, DataDir)));
			}
		}
		else
		{
			ereport(FATAL,
					(errmsg("could not locate required checkpoint record"),
					 errhint("If you are restoring from a backup, touch \"%s/recovery.signal\" and add required recovery options.\n"
							 "If you are not restoring from a backup, try removing the file \"%s/backup_label\".\n"
							 "Be careful: removing \"%s/backup_label\" will result in a corrupt cluster if restoring from a backup.",
							 DataDir, DataDir, DataDir)));
			fc_wasShutdown = false;	/* 保持编译器安静 */
		}

		/* 如果存在，读取 tablespace_map 文件并创建符号链接。 */
		if (fc_read_tablespace_map(&fc_tablespaces))
		{
			ListCell   *fc_lc;

			foreach(fc_lc, fc_tablespaces)
			{
				tablespaceinfo *fc_ti = lfirst(fc_lc);
				char	   *fc_linkloc;

				fc_linkloc = psprintf("pg_tblspc/%s", fc_ti->oid);

				/*
				 * 如果存在，移除现有的符号链接，并在 PGDATA 下创建符号链接。
				 */
				remove_tablespace_symlink(fc_linkloc);

				if (symlink(fc_ti->path, fc_linkloc) < 0)
					ereport(ERROR,
							(errcode_for_file_access(),
							 errmsg("could not create symbolic link \"%s\": %m",
									fc_linkloc)));

				pfree(fc_ti->oid);
				pfree(fc_ti->path);
				pfree(fc_ti);
			}

			/* 告诉调用者稍后删除它 */
			fc_haveTblspcMap = true;
		}

		/* 告诉调用者稍后删除它 */
		fc_haveBackupLabel = true;
	}
	else
	{
		/*
		 * 如果存在 tablespace_map 文件而没有 backup_label 文件，那么
		 * 该文件就没有用处。保留它没有害处，但最好还是删除映射文件，以免在数据目录中
		 * 遗留任何冗余文件，避免任何混淆。不过，简单地将文件重命名到不显眼的位置
		 * 似乎更为谨慎，而不是完全删除它，我们也忽略重命名操作中发生的任何错误，因为
		 * 即使存在映射文件而没有 backup_label 文件，也没有害处。
		 */
		if (stat(TABLESPACE_MAP, &fc_st) == 0)
		{
			unlink(TABLESPACE_MAP_OLD);
			if (durable_rename(TABLESPACE_MAP, TABLESPACE_MAP_OLD, DEBUG1) == 0)
				ereport(LOG,
						(errmsg("ignoring file \"%s\" because no file \"%s\" exists",
								TABLESPACE_MAP, BACKUP_LABEL_FILE),
						 errdetail("File \"%s\" was renamed to \"%s\".",
								   TABLESPACE_MAP, TABLESPACE_MAP_OLD)));
			else
				ereport(LOG,
						(errmsg("ignoring file \"%s\" because no file \"%s\" exists",
								TABLESPACE_MAP, BACKUP_LABEL_FILE),
						 errdetail("Could not rename file \"%s\" to \"%s\": %m.",
								   TABLESPACE_MAP, TABLESPACE_MAP_OLD)));
		}

		/*
		 * 有可能请求了归档恢复，但我们不知道在达到一致性之前需要重放多少 WAL。
		 * 例如，如果从运行中的服务器使用原子文件系统快照进行了基础备份，
		 * 而没有调用 pg_backup_start/stop，或者如果你只是杀死一个正在运行的主服务器
		 * 并通过创建恢复信号文件将其放入归档恢复中，都会发生这种情况。
		 *
		 * 在这种情况下，我们的策略是首先执行崩溃恢复，
		 * 重放 pg_wal 中的所有 WAL，然后再进入归档恢复。
		 *
		 * 但通常我们已经知道需要重放多少 WAL（直到 minRecoveryPoint、直到 backupEndPoint，或者
		 * 直到看到备份结束记录），我们可以直接进入归档恢复。
		 */
		if (ArchiveRecoveryRequested &&
			(fc_ControlFile->minRecoveryPoint != InvalidXLogRecPtr ||
			 fc_ControlFile->backupEndRequired ||
			 fc_ControlFile->backupEndPoint != InvalidXLogRecPtr ||
			 fc_ControlFile->state == DB_SHUTDOWNED))
		{
			InArchiveRecovery = true;
			if (StandbyModeRequested)
				fc_EnableStandbyMode();
		}

		/*
		 * 出于与从存在 backup_label 的基础备份继续初始化时相同的原因，
		 * 当我们继续从基础备份初始化时发出日志消息。
		 */
		if (!XLogRecPtrIsInvalid(fc_ControlFile->backupStartPoint))
			ereport(LOG,
					(errmsg("restarting backup recovery with redo LSN %X/%X",
							LSN_FORMAT_ARGS(fc_ControlFile->backupStartPoint))));

		/* 获取最后一个有效的检查点记录。 */
		CheckPointLoc = fc_ControlFile->checkPoint;
		CheckPointTLI = fc_ControlFile->checkPointCopy.ThisTimeLineID;
		RedoStartLSN = fc_ControlFile->checkPointCopy.redo;
		RedoStartTLI = fc_ControlFile->checkPointCopy.ThisTimeLineID;
		fc_record = fc_ReadCheckpointRecord(xlogprefetcher, CheckPointLoc, 1, true,
									  CheckPointTLI);
		if (fc_record != NULL)
		{
			ereport(DEBUG1,
					(errmsg_internal("checkpoint record is at %X/%X",
									 LSN_FORMAT_ARGS(CheckPointLoc))));
		}
		else
		{
			/*
			 * 我们曾经试图在这里回到次级检查点记录，但仅在未处于备用模式时。
			 * 如果无法读取最后一个检查点，我们现在就会失败，因为这样可以简化
			 * 关于检查点的处理。
			 */
			ereport(PANIC,
					(errmsg("could not locate a valid checkpoint record")));
		}
		memcpy(&fc_checkPoint, XLogRecGetData(xlogreader), sizeof(CheckPoint));
		fc_wasShutdown = ((fc_record->xl_info & ~XLR_INFO_MASK) == XLOG_CHECKPOINT_SHUTDOWN);
	}

	/*
	 * 如果检查点记录的位置不在请求时间线的历史上预期的时间线上，
	 * 我们无法继续：备份不是请求时间线历史的一部分。
	 */
	Assert(expectedTLEs);		/* 由读取检查点记录初始化 */
	if (tliOfPointInHistory(CheckPointLoc, expectedTLEs) !=
		CheckPointTLI)
	{
		XLogRecPtr	fc_switchpoint;

		/*
		 * 如果检查点的时间线根本不在 expectedTLEs 中，tliSwitchPoint
		 * 将抛出一个错误。
		 */
		fc_switchpoint = tliSwitchPoint(fc_ControlFile->checkPointCopy.ThisTimeLineID, expectedTLEs, NULL);
		ereport(FATAL,
				(errmsg("requested timeline %u is not a child of this server's history",
						recoveryTargetTLI),
				 errdetail("Latest checkpoint is at %X/%X on timeline %u, but in the history of the requested timeline, the server forked off from that timeline at %X/%X.",
						   LSN_FORMAT_ARGS(fc_ControlFile->checkPoint),
						   fc_ControlFile->checkPointCopy.ThisTimeLineID,
						   LSN_FORMAT_ARGS(fc_switchpoint))));
	}

	/*
	 * 最小恢复点也应该是请求时间线历史的一部分。
	 */
	if (!XLogRecPtrIsInvalid(fc_ControlFile->minRecoveryPoint) &&
		tliOfPointInHistory(fc_ControlFile->minRecoveryPoint - 1, expectedTLEs) !=
		fc_ControlFile->minRecoveryPointTLI)
		ereport(FATAL,
				(errmsg("requested timeline %u does not contain minimum recovery point %X/%X on timeline %u",
						recoveryTargetTLI,
						LSN_FORMAT_ARGS(fc_ControlFile->minRecoveryPoint),
						fc_ControlFile->minRecoveryPointTLI)));

	ereport(DEBUG1,
			(errmsg_internal("redo record is at %X/%X; shutdown %s",
							 LSN_FORMAT_ARGS(fc_checkPoint.redo),
							 fc_wasShutdown ? "true" : "false")));
	ereport(DEBUG1,
			(errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: %u",
							 U64FromFullTransactionId(fc_checkPoint.nextXid),
							 fc_checkPoint.nextOid)));
	ereport(DEBUG1,
			(errmsg_internal("next MultiXactId: %u; next MultiXactOffset: %u",
							 fc_checkPoint.nextMulti, fc_checkPoint.nextMultiOffset)));
	ereport(DEBUG1,
			(errmsg_internal("oldest unfrozen transaction ID: %u, in database %u",
							 fc_checkPoint.oldestXid, fc_checkPoint.oldestXidDB)));
	ereport(DEBUG1,
			(errmsg_internal("oldest MultiXactId: %u, in database %u",
							 fc_checkPoint.oldestMulti, fc_checkPoint.oldestMultiDB)));
	ereport(DEBUG1,
			(errmsg_internal("commit timestamp Xid oldest/newest: %u/%u",
							 fc_checkPoint.oldestCommitTsXid,
							 fc_checkPoint.newestCommitTsXid)));
	if (!TransactionIdIsNormal(XidFromFullTransactionId(fc_checkPoint.nextXid)))
		ereport(PANIC,
				(errmsg("invalid next transaction ID")));

	/* 计算前一个数据项的长度 */
	if (fc_checkPoint.redo > CheckPointLoc)
		ereport(PANIC,
				(errmsg("invalid redo in checkpoint record")));

	/*
	 * 检查是否需要强制从WAL恢复。如果看起来是干净关机且
	 * 我们没有恢复信号文件，则假设不需要恢复。
	 */
	if (fc_checkPoint.redo < CheckPointLoc)
	{
		if (fc_wasShutdown)
			ereport(PANIC,
					(errmsg("invalid redo record in shutdown checkpoint")));
		InRecovery = true;
	}
	else if (fc_ControlFile->state != DB_SHUTDOWNED)
		InRecovery = true;
	else if (ArchiveRecoveryRequested)
	{
		/* 因恢复信号文件的存在而强制恢复 */
		InRecovery = true;
	}

	/*
	 * 如果需要恢复，请更新内存中pg_control的副本，以显示
	 * 我们正在恢复，并显示所选的检查点作为我们开始的地方。
	 * 我们还会用从备份历史文件中获得的任何最小恢复停止点标记pg_control。
	 *
	 * 不过，我们还不将更改写入磁盘。只有在初始化各种
	 * 子系统后才会这样做。
	 */
	if (InRecovery)
	{
		if (InArchiveRecovery)
		{
			fc_ControlFile->state = DB_IN_ARCHIVE_RECOVERY;
		}
		else
		{
			ereport(LOG,
					(errmsg("database system was not properly shut down; "
							"automatic recovery in progress")));
			if (recoveryTargetTLI > fc_ControlFile->checkPointCopy.ThisTimeLineID)
				ereport(LOG,
						(errmsg("crash recovery starts in timeline %u "
								"and has target timeline %u",
								fc_ControlFile->checkPointCopy.ThisTimeLineID,
								recoveryTargetTLI)));
			fc_ControlFile->state = DB_IN_CRASH_RECOVERY;
		}
		fc_ControlFile->checkPoint = CheckPointLoc;
		fc_ControlFile->checkPointCopy = fc_checkPoint;
		if (InArchiveRecovery)
		{
			/* 如果尚未设置，则初始化minRecoveryPoint */
			if (fc_ControlFile->minRecoveryPoint < fc_checkPoint.redo)
			{
				fc_ControlFile->minRecoveryPoint = fc_checkPoint.redo;
				fc_ControlFile->minRecoveryPointTLI = fc_checkPoint.ThisTimeLineID;
			}
		}

		/*
		 * 如果我们从基础备份开始恢复，则设置backupStartPoint。
		 *
		 * 如果我们从从备用服务器上进行的基础备份开始恢复，则
		 * 还要设置backupEndPoint，并将minRecoveryPoint用作备份结束
		 * 位置。在这种情况下，pg_control中的数据库系统状态必须指示
		 * 数据库已经在恢复中。通常情况下，这将是DB_IN_ARCHIVE_RECOVERY，
		 * 也可能是DB_SHUTDOWNED_IN_RECOVERY，如果恢复以前在达到此点
		 * 之前被中断；例如，因为restore_command或
		 * primary_conninfo有缺陷。
		 *
		 * 任何其他状态都表明备份在某种程度上已经损坏，
		 * 我们无法合理地继续恢复。
		 */
		if (fc_haveBackupLabel)
		{
			fc_ControlFile->backupStartPoint = fc_checkPoint.redo;
			fc_ControlFile->backupEndRequired = backupEndRequired;

			if (fc_backupFromStandby)
			{
				if (fc_dbstate_at_startup != DB_IN_ARCHIVE_RECOVERY &&
					fc_dbstate_at_startup != DB_SHUTDOWNED_IN_RECOVERY)
					ereport(FATAL,
							(errmsg("backup_label contains data inconsistent with control file"),
							 errhint("This means that the backup is corrupted and you will "
									 "have to use another backup for recovery.")));
				fc_ControlFile->backupEndPoint = fc_ControlFile->minRecoveryPoint;
			}
		}
	}

	/* 记住这些，以便我们知道何时已达到一致性 */
	backupStartPoint = fc_ControlFile->backupStartPoint;
	backupEndRequired = fc_ControlFile->backupEndRequired;
	backupEndPoint = fc_ControlFile->backupEndPoint;
	if (InArchiveRecovery)
	{
		minRecoveryPoint = fc_ControlFile->minRecoveryPoint;
		minRecoveryPointTLI = fc_ControlFile->minRecoveryPointTLI;
	}
	else
	{
		minRecoveryPoint = InvalidXLogRecPtr;
		minRecoveryPointTLI = 0;
	}

	/*
	 * 假设最终记录没有丢失，开始恢复。
	 */
	abortedRecPtr = InvalidXLogRecPtr;
	missingContrecPtr = InvalidXLogRecPtr;

	*fc_wasShutdown_ptr = fc_wasShutdown;
	*fc_haveBackupLabel_ptr = fc_haveBackupLabel;
	*fc_haveTblspcMap_ptr = fc_haveTblspcMap;
}

/*
 * 查看是否有任何恢复信号文件，如果有，则设置恢复状态。
 *
 * 查看是否有恢复命令文件（recovery.conf），如果有，则抛出
 * ERROR，因为从PG12开始我们不再识别该文件。
 */
static void fc_readRecoverySignalFile(void)
{
	struct stat fc_stat_buf;

	if (IsBootstrapProcessingMode())
		return;

	/*
	 * 检查旧的恢复API文件：recovery.conf
	 */
	if (stat(RECOVERY_COMMAND_FILE, &fc_stat_buf) == 0)
		ereport(FATAL,
				(errcode_for_file_access(),
				 errmsg("using recovery command file \"%s\" is not supported",
						RECOVERY_COMMAND_FILE)));

	/*
	 * 如果存在，则删除未使用的.done文件。如果不存在，则忽略。
	 */
	unlink(RECOVERY_COMMAND_DONE);

	/*
	 * 检查恢复信号文件，如果找到，则fsync它们，因为它们
	 * 代表服务器状态信息。然而，我们对fsync失败的可能性
	 * 不会太过担心。
	 *
	 * 如果存在，备用信号文件优先。如果两者都不存在，
	 * 则不会进入归档恢复。
	 */
	if (stat(STANDBY_SIGNAL_FILE, &fc_stat_buf) == 0)
	{
		int			fc_fd;

		fc_fd = BasicOpenFilePerm(STANDBY_SIGNAL_FILE, O_RDWR | PG_BINARY,
							   S_IRUSR | S_IWUSR);
		if (fc_fd >= 0)
		{
			(void) pg_fsync(fc_fd);
			close(fc_fd);
		}
		standby_signal_file_found = true;
	}
	else if (stat(RECOVERY_SIGNAL_FILE, &fc_stat_buf) == 0)
	{
		int			fc_fd;

		fc_fd = BasicOpenFilePerm(RECOVERY_SIGNAL_FILE, O_RDWR | PG_BINARY,
							   S_IRUSR | S_IWUSR);
		if (fc_fd >= 0)
		{
			(void) pg_fsync(fc_fd);
			close(fc_fd);
		}
		recovery_signal_file_found = true;
	}

	StandbyModeRequested = false;
	ArchiveRecoveryRequested = false;
	if (standby_signal_file_found)
	{
		StandbyModeRequested = true;
		ArchiveRecoveryRequested = true;
	}
	else if (recovery_signal_file_found)
	{
		StandbyModeRequested = false;
		ArchiveRecoveryRequested = true;
	}
	else
		return;

	/*
	 * 我们不支持独立后端的备用模式；这需要
	 * 其他进程（例如WAL接收器）处于活动状态。
	 */
	if (StandbyModeRequested && !IsUnderPostmaster)
		ereport(FATAL,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("standby mode is not supported by single-user servers")));
}

static void fc_validateRecoveryParameters(void)
{
	if (!ArchiveRecoveryRequested)
		return;

	/*
	 * 检查强制参数
	 */
	if (StandbyModeRequested)
	{
		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
			ereport(WARNING,
					(errmsg("specified neither primary_conninfo nor restore_command"),
					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
	}
	else
	{
		if (recoveryRestoreCommand == NULL ||
			strcmp(recoveryRestoreCommand, "") == 0)
			ereport(FATAL,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("must specify restore_command when standby mode is not enabled")));
	}

	/*
	 * 覆盖任何不一致的请求。请注意，这是9.5中的行为变化；
	 * 在此之前，如果hot_standby = off，我们只是忽略请求暂停，
	 * 这是一种令人惊讶的行为。
	 */
	if (recoveryTargetAction == RECOVERY_TARGET_ACTION_PAUSE &&
		!EnableHotStandby)
		recoveryTargetAction = RECOVERY_TARGET_ACTION_SHUTDOWN;

	/*
	 * 对恢复目标时间字符串的最终解析；另见
	 * check_recovery_target_time()。
	 */
	if (recoveryTarget == RECOVERY_TARGET_TIME)
	{
		recoveryTargetTime = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in,
																	 CStringGetDatum(recovery_target_time_string),
																	 ObjectIdGetDatum(InvalidOid),
																	 Int32GetDatum(-1)));
	}

	/*
	 * 如果用户指定了 recovery_target_timeline，验证它或计算
	 * “最新”值。在我们获得恢复命令并设置 InArchiveRecovery 之前，
	 * 我们无法做到这一点，因为我们需要从存档中获取时间线历史文件。
	 */
	if (recoveryTargetTimeLineGoal == RECOVERY_TARGET_TIMELINE_NUMERIC)
	{
		TimeLineID	fc_rtli = recoveryTargetTLIRequested;

		/* 时间线 1 没有历史文件，其它都应该有 */
		if (fc_rtli != 1 && !existsTimeLineHistory(fc_rtli))
			ereport(FATAL,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("recovery target timeline %u does not exist",
							fc_rtli)));
		recoveryTargetTLI = fc_rtli;
	}
	else if (recoveryTargetTimeLineGoal == RECOVERY_TARGET_TIMELINE_LATEST)
	{
		/* 我们从 pg_control 的时间线开始“最新”搜索 */
		recoveryTargetTLI = findNewestTimeLine(recoveryTargetTLI);
	}
	else
	{
		/*
		 * 否则我们只使用从 ControlFile 中读取的 recoveryTargetTLI
		 */
		Assert(recoveryTargetTimeLineGoal == RECOVERY_TARGET_TIMELINE_CONTROLFILE);
	}
}

/*
 * read_backup_label: 检查是否存在 backup_label 文件
 *
 * 如果我们在恢复过程中看到了 backup_label，我们假设
 * 我们正在从备份转储文件中恢复，因此我们将从标签文件标识的检查点
 * 向前滚动，而不是 pg_control 所说的。这避免了 pg_control 可能
 * 在转储开始后一个或多个检查点后被归档的问题，因此如果我们依赖它
 * 作为起始点，我们将无法恢复一致的数据库状态。
 *
 * 如果找到 backup_label，则返回 true（并将检查点位置和 TLI
 * 填入 *checkPointLoc 和 *backupLabelTLI）；如果没有，则返回 false。
 * 如果这个 backup_label 来自一个流式备份，则 *backupEndRequired 被
 * 设置为 true。如果这个 backup_label 是在恢复过程中创建的，则
 * *backupFromStandby 被设置为 true。
 *
 * 还将全局变量 RedoStartLSN 和 RedoStartTLI 设置为从备份文件
 * 读取的 LSN 和 TLI。
 */
static bool fc_read_backup_label(XLogRecPtr *fc_checkPointLoc, TimeLineID *fc_backupLabelTLI,
				  bool *fc_backupEndRequired, bool *fc_backupFromStandby)
{
	char		fc_startxlogfilename[MAXFNAMELEN];
	TimeLineID	fc_tli_from_walseg,
				fc_tli_from_file;
	FILE	   *fc_lfp;
	char		fc_ch;
	char		fc_backuptype[20];
	char		fc_backupfrom[20];
	char		fc_backuplabel[MAXPGPATH];
	char		fc_backuptime[128];
	uint32		fc_hi,
				fc_lo;

	/* 抑制可能的未初始化变量警告 */
	*fc_checkPointLoc = InvalidXLogRecPtr;
	*fc_backupLabelTLI = 0;
	*fc_backupEndRequired = false;
	*fc_backupFromStandby = false;

	/*
	 * 检查标签文件是否存在
	 */
	fc_lfp = AllocateFile(BACKUP_LABEL_FILE, "r");
	if (!fc_lfp)
	{
		if (errno != ENOENT)
			ereport(FATAL,
					(errcode_for_file_access(),
					 errmsg("could not read file \"%s\": %m",
							BACKUP_LABEL_FILE)));
		return false;			/* 它不存在，一切正常 */
	}

	/*
	 * 读取和解析 START WAL LOCATION 和 CHECKPOINT 行（这段代码
	 * 相当粗糙，但我们不指望文件格式有任何变化）。
	 */
	if (fscanf(fc_lfp, "START WAL LOCATION: %X/%X (file %08X%16s)%c",
			   &fc_hi, &fc_lo, &fc_tli_from_walseg, fc_startxlogfilename, &fc_ch) != 5 || fc_ch != '\n')
		ereport(FATAL,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("invalid data in file \"%s\"", BACKUP_LABEL_FILE)));
	RedoStartLSN = ((uint64) fc_hi) << 32 | fc_lo;
	RedoStartTLI = fc_tli_from_walseg;
	if (fscanf(fc_lfp, "CHECKPOINT LOCATION: %X/%X%c",
			   &fc_hi, &fc_lo, &fc_ch) != 3 || fc_ch != '\n')
		ereport(FATAL,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("invalid data in file \"%s\"", BACKUP_LABEL_FILE)));
	*fc_checkPointLoc = ((uint64) fc_hi) << 32 | fc_lo;
	*fc_backupLabelTLI = fc_tli_from_walseg;

	/*
	 * BACKUP METHOD 让我们知道这是否是典型的备份（“流式”，
	 * 这可能意味着使用了 pg_basebackup 或 pg_backup_start/stop
	 * 方法），或者这个标签是否来自其他地方（今天唯一的
	 * 其他选项是来自 pg_rewind）。如果这是一个流式备份，
	 * 那么我们知道需要播放直到到达在备份过程中生成的 WAL
	 * 的结束（此时我们将达到一致性，backupEndRequired 将被重置为 false）。
	 */
	if (fscanf(fc_lfp, "BACKUP METHOD: %19s\n", fc_backuptype) == 1)
	{
		if (strcmp(fc_backuptype, "streamed") == 0)
			*fc_backupEndRequired = true;
	}

	/*
	 * BACKUP FROM 让我们知道这是否来自主节点或备份节点。
	 * 如果来自备份节点，我们将再次检查控制文件状态
	 * 是否与备份节点相匹配。
	 */
	if (fscanf(fc_lfp, "BACKUP FROM: %19s\n", fc_backupfrom) == 1)
	{
		if (strcmp(fc_backupfrom, "standby") == 0)
			*fc_backupFromStandby = true;
	}

	/*
	 * 解析开始时间和标签。这些不是恢复的强制字段，
	 * 但检查它们的存在对于调试和下一步的
	 * 一致性检查是有用的。还要处理结果缓冲区具有
	 * 预分配大小的事实，因此，如果 backup_label 文件生成
	 * 时包含的字符串长于此处假设的最大值，则会发生不正确的解析。
	 * 这没关系，因为后续只进行小的兼容性检查。
	 */
	if (fscanf(fc_lfp, "START TIME: %127[^\n]\n", fc_backuptime) == 1)
		ereport(DEBUG1,
				(errmsg_internal("backup time %s in file \"%s\"",
								 fc_backuptime, BACKUP_LABEL_FILE)));

	if (fscanf(fc_lfp, "LABEL: %1023[^\n]\n", fc_backuplabel) == 1)
		ereport(DEBUG1,
				(errmsg_internal("backup label %s in file \"%s\"",
								 fc_backuplabel, BACKUP_LABEL_FILE)));

	/*
	 * START TIMELINE 是自 11 版本以来的新特性。它的解析不是强制性的，
	 * 但如果存在，仍然将其作为一致性检查使用。
	 */
	if (fscanf(fc_lfp, "START TIMELINE: %u\n", &fc_tli_from_file) == 1)
	{
		if (fc_tli_from_walseg != fc_tli_from_file)
			ereport(FATAL,
					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
					 errmsg("invalid data in file \"%s\"", BACKUP_LABEL_FILE),
					 errdetail("Timeline ID parsed is %u, but expected %u.",
							   fc_tli_from_file, fc_tli_from_walseg)));

		ereport(DEBUG1,
				(errmsg_internal("backup timeline %u in file \"%s\"",
								 fc_tli_from_file, BACKUP_LABEL_FILE)));
	}

	if (ferror(fc_lfp) || FreeFile(fc_lfp))
		ereport(FATAL,
				(errcode_for_file_access(),
				 errmsg("could not read file \"%s\": %m",
						BACKUP_LABEL_FILE)));

	return true;
}

/*
 * read_tablespace_map: 检查是否存在 tablespace_map 文件
 *
 * 如果在恢复过程中看到 tablespace_map 文件，我们假设
 * 正在从备份转储文件恢复，因此需要根据 tablespace_map 文件中的信息
 * 创建符号链接。
 *
 * 如果找到 tablespace_map 文件则返回 true（并用
 * tablespaceinfo 结构填充 *tablespaces，以获取文件中列出的每个表空间）；
 * 如果没有则返回 false。
 */
static bool fc_read_tablespace_map(List **fc_tablespaces)
{
	tablespaceinfo *fc_ti;
	FILE	   *fc_lfp;
	char		fc_str[MAXPGPATH];
	int			fc_ch,
				fc_i,
				fc_n;
	bool		fc_was_backslash;

	/*
	 * 查看是否存在 tablespace_map 文件
	 */
	fc_lfp = AllocateFile(TABLESPACE_MAP, "r");
	if (!fc_lfp)
	{
		if (errno != ENOENT)
			ereport(FATAL,
					(errcode_for_file_access(),
					 errmsg("could not read file \"%s\": %m",
							TABLESPACE_MAP)));
		return false;			/* 它不存在，一切正常 */
	}

	/*
	 * 从 tablespace_map 文件中读取和解析链接名称和路径行
	 * （这段代码相当粗糙，但我们不期望文件格式会有任何变化）。
	 * 对插入的任何反斜杠进行取消转义。
	 */
	fc_i = 0;
	fc_was_backslash = false;
	while ((fc_ch = fgetc(fc_lfp)) != EOF)
	{
		if (!fc_was_backslash && (fc_ch == '\n' || fc_ch == '\r'))
		{
			if (fc_i == 0)
				continue;		/* \r 紧接着 \n */

			/*
			 * 取消转义的行应包含一个 OID，后面跟着恰好
			 * 一个空格，然后是一个路径。路径可能以
			 * 空格开头，因此不要过于宽松地解析。
			 */
			fc_str[fc_i] = '\0';
			fc_n = 0;
			while (fc_str[fc_n] && fc_str[fc_n] != ' ')
				fc_n++;
			if (fc_n < 1 || fc_n >= fc_i - 1)
				ereport(FATAL,
						(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
						 errmsg("invalid data in file \"%s\"", TABLESPACE_MAP)));
			fc_str[fc_n++] = '\0';

			fc_ti = palloc0(sizeof(tablespaceinfo));
			fc_ti->oid = pstrdup(fc_str);
			fc_ti->path = pstrdup(fc_str + fc_n);
			*fc_tablespaces = lappend(*fc_tablespaces, fc_ti);

			fc_i = 0;
			continue;
		}
		else if (!fc_was_backslash && fc_ch == '\\')
			fc_was_backslash = true;
		else
		{
			if (fc_i < sizeof(fc_str) - 1)
				fc_str[fc_i++] = fc_ch;
			fc_was_backslash = false;
		}
	}

	if (fc_i != 0 || fc_was_backslash)	/* 最后一行没有结束符？ */
		ereport(FATAL,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("invalid data in file \"%s\"", TABLESPACE_MAP)));

	if (ferror(fc_lfp) || FreeFile(fc_lfp))
		ereport(FATAL,
				(errcode_for_file_access(),
				 errmsg("could not read file \"%s\": %m",
						TABLESPACE_MAP)));

	return true;
}

/*
 * 完成 WAL 恢复。
 *
 * 这并不关闭 'xlogreader'，因为在某些情况下调用者
 * 仍然希望通过调用 ReadCheckPointRecord() 重新读取最后的检查点记录。
 *
 * 返回最后一个有效或已应用记录的位置，之后新的
 * WAL 应该被附加，关于恢复为什么结束的信息，以及一些
 * 其他信息。有关详细信息，请参见 WalRecoveryResult 结构。
 */
EndOfWalRecoveryInfo *
FinishWalRecovery(void)
{
	EndOfWalRecoveryInfo *fc_result = palloc(sizeof(EndOfWalRecoveryInfo));
	XLogRecPtr	fc_lastRec;
	TimeLineID	fc_lastRecTLI;
	XLogRecPtr	fc_endOfLog;

	/*
	 * 在继续写入启动检查点和中止记录之前，杀死 WAL 接收器（如果它仍在运行）。
	 * 如果在我们开始写入 WAL 时它仍然活着，它将覆盖
	 * 这些记录和后续记录。
	 */
	XLogShutdownWalRcv();

	/*
	 * 我们现在完成了从流中读取 xlog。关闭流
	 * 恢复以强制从归档或 pg_wal 中获取文件
	 *（这在恢复结束时是必需的，例如，时间线历史文件）。
	 *
	 * 请注意，在杀死 WAL 接收器后，必须关闭备用模式，
	 * 即调用 XLogShutdownWalRcv()。
	 */
	Assert(!WalRcvStreaming());
	StandbyMode = false;

	/*
	 * 确定下一步写入WAL的位置。
	 *
	 * 重新获取最后一个有效或最后应用的记录，以便我们可以识别
	 * 我们认为的WAL有效部分的确切结束点。之后可能有
	 * 不完整的续写记录，在这种情况下
	 * 'abortedRecPtr'和'missingContrecPtr'会被设置，调用者将
	 * 写入一个特殊的OVERWRITE_CONTRECORD消息以标记其余部分
	 * 是故意缺失的。请参阅CreateOverwriteContrecordRecord()。
	 *
	 * 这一个重要的副作用是将最后一页加载到
	 * xlogreader中。调用者将使用它来初始化WAL进行写入。
	 */
	if (!InRecovery)
	{
		fc_lastRec = CheckPointLoc;
		fc_lastRecTLI = CheckPointTLI;
	}
	else
	{
		fc_lastRec = XLogRecoveryCtl->lastReplayedReadRecPtr;
		fc_lastRecTLI = XLogRecoveryCtl->lastReplayedTLI;
	}
	XLogPrefetcherBeginRead(xlogprefetcher, fc_lastRec);
	(void) fc_ReadRecord(xlogprefetcher, PANIC, false, fc_lastRecTLI);
	fc_endOfLog = xlogreader->EndRecPtr;

	/*
	 * 在包含日志结束的XLOG段的文件名中记住TLI。
	 * 如果在该段中发生了时间线切换，并且我们正在从属于更高
	 * 时间线的段中读取旧WAL，则其可能与endOfLog
	 * 名义上所属的时间线不同。
	 */
	fc_result->endOfLogTLI = xlogreader->seg.ws_tli;

	if (ArchiveRecoveryRequested)
	{
		/*
		 * 我们不再处于归档恢复状态。
		 *
		 * 我们现在已经完成了对旧WAL的读取。如果归档获取
		 * 是活跃的，则停止它。
		 */
		Assert(InArchiveRecovery);
		InArchiveRecovery = false;

		/*
		 * 如果结束日志段仍然打开，则关闭它（以避免
		 * 在尝试重命名或删除打开文件时在Windows上出现问题）。
		 */
		if (readFile >= 0)
		{
			close(readFile);
			readFile = -1;
		}
	}

	/*
	 * 将最后一个部分块复制给调用者，以初始化WAL
	 * 缓冲区以附加新的WAL。
	 */
	if (fc_endOfLog % XLOG_BLCKSZ != 0)
	{
		char	   *fc_page;
		int			fc_len;
		XLogRecPtr	fc_pageBeginPtr;

		fc_pageBeginPtr = fc_endOfLog - (fc_endOfLog % XLOG_BLCKSZ);
		Assert(readOff == XLogSegmentOffset(fc_pageBeginPtr, wal_segment_size));

		/* 复制最后块的有效部分 */
		fc_len = fc_endOfLog % XLOG_BLCKSZ;
		fc_page = palloc(fc_len);
		memcpy(fc_page, xlogreader->readBuf, fc_len);

		fc_result->lastPageBeginPtr = fc_pageBeginPtr;
		fc_result->lastPage = fc_page;
	}
	else
	{
		/* 没有部分块可供复制。 */
		fc_result->lastPageBeginPtr = fc_endOfLog;
		fc_result->lastPage = NULL;
	}

	/*
	 * 为历史文件创建注释，以解释时间线
	 * 更改的原因和位置。
	 */
	fc_result->recoveryStopReason = fc_getRecoveryStopReason();

	fc_result->lastRec = fc_lastRec;
	fc_result->lastRecTLI = fc_lastRecTLI;
	fc_result->endOfLog = fc_endOfLog;

	fc_result->abortedRecPtr = abortedRecPtr;
	fc_result->missingContrecPtr = missingContrecPtr;

	fc_result->standby_signal_file_found = standby_signal_file_found;
	fc_result->recovery_signal_file_found = recovery_signal_file_found;

	return fc_result;
}

/*
 * 清理WAL读取器和从归档恢复WAL的剩余部分
 */
void ShutdownWalRecovery(void)
{
	char		fc_recoveryPath[MAXPGPATH];

	/* 对pg_stat_recovery_prefetch的最终更新。 */
	XLogPrefetcherComputeStats(xlogprefetcher);

	/* 关闭xlogreader */
	if (readFile >= 0)
	{
		close(readFile);
		readFile = -1;
	}
	XLogReaderFree(xlogreader);
	XLogPrefetcherFree(xlogprefetcher);

	if (ArchiveRecoveryRequested)
	{
		/*
		 * 由于可能存在名为RECOVERYXLOG的部分WAL段，处理
		 * 它。
		 */
		snprintf(fc_recoveryPath, MAXPGPATH, XLOGDIR "/RECOVERYXLOG");
		unlink(fc_recoveryPath);	/* 忽略任何错误 */

		/* 处理任何剩余的恢复时间线历史文件 */
		snprintf(fc_recoveryPath, MAXPGPATH, XLOGDIR "/RECOVERYHISTORY");
		unlink(fc_recoveryPath);	/* 忽略任何错误 */
	}

	/*
	 * 我们不再需要这个闩锁。严格来说不必放弃
	 * 它，但为了整洁起见，让我们这样做。
	 */
	if (ArchiveRecoveryRequested)
		DisownLatch(&XLogRecoveryCtl->recoveryWakeupLatch);
}

/*
 * 执行WAL恢复。
 *
 * 如果系统是干净 shutdown 的，这个函数就不会被调用。
 */
void PerformWalRecovery(void)
{
	XLogRecord *fc_record;
	bool		fc_reachedRecoveryTarget = false;
	TimeLineID	fc_replayTLI;

	/*
	 * 初始化用于跟踪WAL重放进度的共享变量，就好像
	 * 我们刚刚重放了REDO位置之前的记录（或检查点记录本身，
	 * 如果这是一个关闭检查点）。
	 */
	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	if (RedoStartLSN < CheckPointLoc)
	{
		XLogRecoveryCtl->lastReplayedReadRecPtr = InvalidXLogRecPtr;
		XLogRecoveryCtl->lastReplayedEndRecPtr = RedoStartLSN;
		XLogRecoveryCtl->lastReplayedTLI = RedoStartTLI;
	}
	else
	{
		XLogRecoveryCtl->lastReplayedReadRecPtr = xlogreader->ReadRecPtr;
		XLogRecoveryCtl->lastReplayedEndRecPtr = xlogreader->EndRecPtr;
		XLogRecoveryCtl->lastReplayedTLI = CheckPointTLI;
	}
	XLogRecoveryCtl->replayEndRecPtr = XLogRecoveryCtl->lastReplayedEndRecPtr;
	XLogRecoveryCtl->replayEndTLI = XLogRecoveryCtl->lastReplayedTLI;
	XLogRecoveryCtl->recoveryLastXTime = 0;
	XLogRecoveryCtl->currentChunkStartTime = 0;
	XLogRecoveryCtl->recoveryPauseState = RECOVERY_NOT_PAUSED;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	/* 还确保XLogReceiptTime有一个合理的值 */
	XLogReceiptTime = GetCurrentTimestamp();

	/*
	 * 让postmaster知道我们已经开始重放，以便它在必要时
	 * 可以启动归档程序。
	 */
	if (IsUnderPostmaster)
		SendPostmasterSignal(PMSIGNAL_RECOVERY_STARTED);

	/*
	 * 如果我们已经一致，则立即允许只读连接。
	 */
	fc_CheckRecoveryConsistency();

	/*
	 * 找到逻辑上跟随检查点的第一条记录---它
	 * 可能在物理上位于它之前。
	 */
	if (RedoStartLSN < CheckPointLoc)
	{
		/* 向后查找记录 */
		fc_replayTLI = RedoStartTLI;
		XLogPrefetcherBeginRead(xlogprefetcher, RedoStartLSN);
		fc_record = fc_ReadRecord(xlogprefetcher, PANIC, false, fc_replayTLI);
	}
	else
	{
		/* 只需在检查点后读取下一条记录 */
		Assert(xlogreader->ReadRecPtr == CheckPointLoc);
		fc_replayTLI = CheckPointTLI;
		fc_record = fc_ReadRecord(xlogprefetcher, LOG, false, fc_replayTLI);
	}

	if (fc_record != NULL)
	{
		TimestampTz fc_xtime;
		PGRUsage	fc_ru0;

		pg_rusage_init(&fc_ru0);

		InRedo = true;

		RmgrStartup();

		ereport(LOG,
				(errmsg("redo starts at %X/%X",
						LSN_FORMAT_ARGS(xlogreader->ReadRecPtr))));

		/* 准备报告重做阶段的进度。 */
		if (!StandbyMode)
			begin_startup_progress_phase();

		/*
		 * 主重做应用循环
		 */
		do
		{
			if (!StandbyMode)
				ereport_startup_progress("redo in progress, elapsed time: %ld.%02d s, current LSN: %X/%X",
										 LSN_FORMAT_ARGS(xlogreader->ReadRecPtr));

#ifdef WAL_DEBUG
			if (XLOG_DEBUG ||
				(fc_record->xl_rmid == RM_XACT_ID && trace_recovery_messages <= DEBUG2) ||
				(fc_record->xl_rmid != RM_XACT_ID && trace_recovery_messages <= DEBUG3))
			{
				StringInfoData fc_buf;

				initStringInfo(&fc_buf);
				appendStringInfo(&fc_buf, "REDO @ %X/%X; LSN %X/%X: ",
								 LSN_FORMAT_ARGS(xlogreader->ReadRecPtr),
								 LSN_FORMAT_ARGS(xlogreader->EndRecPtr));
				fc_xlog_outrec(&fc_buf, xlogreader);
				appendStringInfoString(&fc_buf, " - ");
				xlog_outdesc(&fc_buf, xlogreader);
				elog(LOG, "%s", fc_buf.data);
				pfree(fc_buf.data);
			}
#endif

			/* 处理启动进程的中断信号 */
			HandleStartupProcInterrupts();

			/*
			 * 暂停WAL重放，如果有热备份会话通过
			 * SetRecoveryPause()请求暂停。
			 *
			 * 请注意，我们故意不在这里获取info_lck自旋锁。
			 * 因此，我们可能读取到稍微陈旧的recoveryPause标志的值，但
			 * 它不会很陈旧（不比我们之前获取的最后一个自旋锁更陈旧）。
			 * 由于暂停请求在任何情况下都是一种相当异步的行为，
			 * 可能比我们本来会响应的晚一个WAL记录响应它也是一个次要问题，
			 * 所以似乎不值得添加另一个自旋锁周期来防止这种情况。
			 */
			if (((volatile XLogRecoveryCtlData *) XLogRecoveryCtl)->recoveryPauseState !=
				RECOVERY_NOT_PAUSED)
				fc_recoveryPausesHere(false);

			/*
			 * 我们是否达到了恢复目标？
			 */
			if (fc_recoveryStopsBefore(xlogreader))
			{
				fc_reachedRecoveryTarget = true;
				break;
			}

			/*
			 * 如果我们被要求滞后于主服务器，等待锁存直到
			 * 足够的时间过去。
			 */
			if (fc_recoveryApplyDelay(xlogreader))
			{
				/*
				 * 我们在这里再次测试暂停恢复。如果用户设置了
				 * 延迟应用，可能是因为他们期望在出现问题时暂停
				 * 恢复，所以我们必须在这里再次测试，
				 * 否则在延迟等待期间暂停将不起作用。
				 */
				if (((volatile XLogRecoveryCtlData *) XLogRecoveryCtl)->recoveryPauseState !=
					RECOVERY_NOT_PAUSED)
					fc_recoveryPausesHere(false);
			}

			/*
			 * 应用记录
			 */
			fc_ApplyWalRecord(xlogreader, fc_record, &fc_replayTLI);

			/* 如果我们达到了包含的恢复目标则退出循环 */
			if (fc_recoveryStopsAfter(xlogreader))
			{
				fc_reachedRecoveryTarget = true;
				break;
			}

			/* 否则，尝试获取下一个WAL记录 */
			fc_record = fc_ReadRecord(xlogprefetcher, LOG, false, fc_replayTLI);
		} while (fc_record != NULL);

		/*
		 * 主重做应用循环结束
		 */

		if (fc_reachedRecoveryTarget)
		{
			if (!reachedConsistency)
				ereport(FATAL,
						(errmsg("requested recovery stop point is before consistent recovery point")));

			/*
			 * 这是我们能够用新的恢复目标重新开始恢复的最后一个点，
			 * 如果我们关闭并重新开始。在此之后，
			 * 资源管理器可能会选择在恢复结束时采取永久的纠正措施。
			 */
			switch (recoveryTargetAction)
			{
				case RECOVERY_TARGET_ACTION_SHUTDOWN:

					/*
					 * 使用特殊返回代码请求关闭
					 * postmaster。记录从postmaster发出的日志消息。
					 */
					proc_exit(3);

				case RECOVERY_TARGET_ACTION_PAUSE:
					SetRecoveryPause(true);
					fc_recoveryPausesHere(true);

					/* 进入升级模式 */

				case RECOVERY_TARGET_ACTION_PROMOTE:
					break;
			}
		}

		RmgrCleanup();

		ereport(LOG,
				(errmsg("redo done at %X/%X system usage: %s",
						LSN_FORMAT_ARGS(xlogreader->ReadRecPtr),
						pg_rusage_show(&fc_ru0))));
		fc_xtime = GetLatestXTime();
		if (fc_xtime)
			ereport(LOG,
					(errmsg("last completed transaction was at log time %s",
							timestamptz_to_str(fc_xtime))));

		InRedo = false;
	}
	else
	{
		/* 检查点之后没有WAL记录 */
		ereport(LOG,
				(errmsg("redo is not required")));
	}

	/*
	 * 此检查故意放在上面的日志消息之后，这些消息指示
	 * 恢复进行了多远。
	 */
	if (ArchiveRecoveryRequested &&
		recoveryTarget != RECOVERY_TARGET_UNSET &&
		!fc_reachedRecoveryTarget)
		ereport(FATAL,
				(errmsg("recovery ended before configured recovery target was reached")));
}

/*
 * PerformWalRecovery的子例程，用于应用一个WAL记录。
 */
static void fc_ApplyWalRecord(XLogReaderState *fc_xlogreader, XLogRecord *fc_record, TimeLineID *fc_replayTLI)
{
	ErrorContextCallback fc_errcallback;
	bool		fc_switchedTLI = false;

	/* 为ereport()设置错误追踪支持 */
	fc_errcallback.callback = fc_rm_redo_error_callback;
	fc_errcallback.arg = (void *) fc_xlogreader;
	fc_errcallback.previous = error_context_stack;
	error_context_stack = &fc_errcallback;

	/*
	 * ShmemVariableCache->nextXid必须超过记录的xid。
	 */
	AdvanceNextFullTransactionIdPastXid(fc_record->xl_xid);

	/*
	 * 在重放此记录之前，检查此记录是否导致当前
	 * 时序线发生变化。该记录已经被视为新时序线的一部分，
	 * 因此我们在重放之前更新replayTLI。这很重要，
	 * 以便在重放此记录后如果恢复停止，记录为最小
	 * 恢复点的TLI的replayEndTLI能够正确设置。
	 */
	if (fc_record->xl_rmid == RM_XLOG_ID)
	{
		TimeLineID	fc_newReplayTLI = *fc_replayTLI;
		TimeLineID	fc_prevReplayTLI = *fc_replayTLI;
		uint8		fc_info = fc_record->xl_info & ~XLR_INFO_MASK;

		if (fc_info == XLOG_CHECKPOINT_SHUTDOWN)
		{
			CheckPoint	fc_checkPoint;

			memcpy(&fc_checkPoint, XLogRecGetData(fc_xlogreader), sizeof(CheckPoint));
			fc_newReplayTLI = fc_checkPoint.ThisTimeLineID;
			fc_prevReplayTLI = fc_checkPoint.PrevTimeLineID;
		}
		else if (fc_info == XLOG_END_OF_RECOVERY)
		{
			xl_end_of_recovery fc_xlrec;

			memcpy(&fc_xlrec, XLogRecGetData(fc_xlogreader), sizeof(xl_end_of_recovery));
			fc_newReplayTLI = fc_xlrec.ThisTimeLineID;
			fc_prevReplayTLI = fc_xlrec.PrevTimeLineID;
		}

		if (fc_newReplayTLI != *fc_replayTLI)
		{
			/* 检查切换到此TLI是否可行 */
			fc_checkTimeLineSwitch(fc_xlogreader->EndRecPtr,
								fc_newReplayTLI, fc_prevReplayTLI, *fc_replayTLI);

			/* 后续的WAL记录应使用新TLI运行 */
			*fc_replayTLI = fc_newReplayTLI;
			fc_switchedTLI = true;
		}
	}

	/*
	 * 在重放此记录之前更新共享replayEndRecPtr，
	 * 以便XLogFlush可以正确更新minRecoveryPoint。
	 */
	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	XLogRecoveryCtl->replayEndRecPtr = fc_xlogreader->EndRecPtr;
	XLogRecoveryCtl->replayEndTLI = *fc_replayTLI;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	/*
	 * 如果我们试图进入热备份模式，处理我们看到的XID
	 */
	if (standbyState >= STANDBY_INITIALIZED &&
		TransactionIdIsValid(fc_record->xl_xid))
		RecordKnownAssignedTransactionIds(fc_record->xl_xid);

	/*
	 * 一些与恢复相关的XLOG记录类型直接在这里处理，
	 * 而不是在xlog_redo()中处理。
	 */
	if (fc_record->xl_rmid == RM_XLOG_ID)
		fc_xlogrecovery_redo(fc_xlogreader, *fc_replayTLI);

	/* 现在应用WAL记录本身 */
	GetRmgr(fc_record->xl_rmid).rm_redo(fc_xlogreader);

	/*
	 * 在重做之后，检查与WAL记录关联的备份页面是否与现有页面一致。只有在为此记录启用一致性检查时，才会进行此检查。
	 */
	if ((fc_record->xl_info & XLR_CHECK_CONSISTENCY) != 0)
		fc_verifyBackupPageConsistency(fc_xlogreader);

	/* 弹出错误上下文栈 */
	error_context_stack = fc_errcallback.previous;

	/*
	 * 在该记录成功重放后更新lastReplayedEndRecPtr。
	 */
	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	XLogRecoveryCtl->lastReplayedReadRecPtr = fc_xlogreader->ReadRecPtr;
	XLogRecoveryCtl->lastReplayedEndRecPtr = fc_xlogreader->EndRecPtr;
	XLogRecoveryCtl->lastReplayedTLI = *fc_replayTLI;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	/*
	 * 如果rm_redo调用了XLogRequestWalReceiverReply，那么我们唤醒接收者，以便它注意到更新后的lastReplayedEndRecPtr并向主服务器发送回复。
	 */
	if (doRequestWalReceiverReply)
	{
		doRequestWalReceiverReply = false;
		WalRcvForceReply();
	}

	/* 如果我们现在一致，则允许只读连接 */
	fc_CheckRecoveryConsistency();

	/* 这是时间线切换吗？ */
	if (fc_switchedTLI)
	{
		/*
		 * 在我们继续新时间线之前，清理旧时间线上的任何（可能是虚假的）未来WAL段。
		 */
		RemoveNonParentXlogFiles(fc_xlogreader->EndRecPtr, *fc_replayTLI);

		/*
		 * 唤醒任何walsender，以便注意到我们处于新时间线。
		 */
		if (AllowCascadeReplication())
			WalSndWakeup();

		/* 重置预取器。 */
		XLogPrefetchReconfigure();
	}
}

/*
 * 一些直接与WAL恢复相关的XLOG RM记录类型在这里处理，而不是在xlog_redo()
 */
static void fc_xlogrecovery_redo(XLogReaderState *fc_record, TimeLineID fc_replayTLI)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;

	Assert(XLogRecGetRmid(fc_record) == RM_XLOG_ID);

	if (fc_info == XLOG_OVERWRITE_CONTRECORD)
	{
		/* 验证XLOG_OVERWRITE_CONTRECORD记录的有效负载。 */
		xl_overwrite_contrecord fc_xlrec;

		memcpy(&fc_xlrec, XLogRecGetData(fc_record), sizeof(xl_overwrite_contrecord));
		if (fc_xlrec.overwritten_lsn != fc_record->overwrittenRecPtr)
			elog(FATAL, "mismatching overwritten LSN %X/%X -> %X/%X",
				 LSN_FORMAT_ARGS(fc_xlrec.overwritten_lsn),
				 LSN_FORMAT_ARGS(fc_record->overwrittenRecPtr));

		/* 我们已安全跳过已中止的记录 */
		abortedRecPtr = InvalidXLogRecPtr;
		missingContrecPtr = InvalidXLogRecPtr;

		ereport(LOG,
				(errmsg("successfully skipped missing contrecord at %X/%X, overwritten at %s",
						LSN_FORMAT_ARGS(fc_xlrec.overwritten_lsn),
						timestamptz_to_str(fc_xlrec.overwrite_time))));

		/* 验证记录应仅发生一次 */
		fc_record->overwrittenRecPtr = InvalidXLogRecPtr;
	}
	else if (fc_info == XLOG_BACKUP_END)
	{
		XLogRecPtr	fc_startpoint;

		memcpy(&fc_startpoint, XLogRecGetData(fc_record), sizeof(fc_startpoint));

		if (backupStartPoint == fc_startpoint)
		{
			/*
			 * 我们已达到基础备份的结束，pg_backup_stop()完成的位置。磁盘上的数据现在是一致的（假设我们也已达到minRecoveryPoint）。将backupEndPoint设置为当前LSN，以便下一次调用CheckRecoveryConsistency()时会注意到并执行备份结束处理。
			 */
			elog(DEBUG1, "end of backup record reached");

			backupEndPoint = fc_lsn;
		}
		else
			elog(DEBUG1, "saw end-of-backup record for backup starting at %X/%X, waiting for %X/%X",
				 LSN_FORMAT_ARGS(fc_startpoint), LSN_FORMAT_ARGS(backupStartPoint));
	}
}

/*
 * 验证在非测试模式下，./pg_tblspc不包含任何真实目录。
 *
 * 对后来的数据库创建XLOG记录的重放会在pg_tblspc中创建虚假目录。到达一致性时，这些目录应该已经被删除；在这里我们验证这一点确实发生了。这是在达到一致状态时调用的。
 *
 * allow_in_place_tablespaces将PANIC转变为WARNING，这对测试目的很有用，并且在出现问题时也允许逃生路径。
 */
static void fc_CheckTablespaceDirectory(void)
{
	DIR		   *fc_dir;
	struct dirent *fc_de;

	fc_dir = AllocateDir("pg_tblspc");
	while ((fc_de = ReadDir(fc_dir, "pg_tblspc")) != NULL)
	{
		char		fc_path[MAXPGPATH + 10];

		/* 跳过非OID名称的条目 */
		if (strspn(fc_de->d_name, "0123456789") != strlen(fc_de->d_name))
			continue;

		snprintf(fc_path, sizeof(fc_path), "pg_tblspc/%s", fc_de->d_name);

		if (get_dirent_type(fc_path, fc_de, false, ERROR) != PGFILETYPE_LNK)
			ereport(allow_in_place_tablespaces ? WARNING : PANIC,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("unexpected directory entry \"%s\" found in %s",
							fc_de->d_name, "pg_tblspc/"),
					 errdetail("All directory entries in pg_tblspc/ should be symbolic links."),
					 errhint("Remove those directories, or set allow_in_place_tablespaces to ON transiently to let recovery complete.")));
	}
}

/*
 * 检查恢复是否已达到一致状态。当一致性达到并且我们有有效的起始备用快照时，告诉postmaster它可以开始接受只读连接。
 */
static void fc_CheckRecoveryConsistency(void)
{
	XLogRecPtr	fc_lastReplayedEndRecPtr;
	TimeLineID	fc_lastReplayedTLI;

	/*
	 * 在崩溃恢复期间，我们在重放所有WAL之前不会达到一致状态。
	 */
	if (XLogRecPtrIsInvalid(minRecoveryPoint))
		return;

	Assert(InArchiveRecovery);

	/*
	 * 假设我们在启动过程中被调用，因此无需锁定以读取lastReplayedEndRecPtr
	 */
	fc_lastReplayedEndRecPtr = XLogRecoveryCtl->lastReplayedEndRecPtr;
	fc_lastReplayedTLI = XLogRecoveryCtl->lastReplayedTLI;

	/*
	 * 我们是否已达到基础备份完成的点？
	 */
	if (!XLogRecPtrIsInvalid(backupEndPoint) &&
		backupEndPoint <= fc_lastReplayedEndRecPtr)
	{
		XLogRecPtr	fc_saveBackupStartPoint = backupStartPoint;
		XLogRecPtr	fc_saveBackupEndPoint = backupEndPoint;

		elog(DEBUG1, "end of backup reached");

		/*
		 * 我们已达到基础备份的结束，如pg_control所示。相应地更新控制文件。
		 */
		ReachedEndOfBackup(fc_lastReplayedEndRecPtr, fc_lastReplayedTLI);
		backupStartPoint = InvalidXLogRecPtr;
		backupEndPoint = InvalidXLogRecPtr;
		backupEndRequired = false;

		ereport(LOG,
				(errmsg("completed backup recovery with redo LSN %X/%X and end LSN %X/%X",
						LSN_FORMAT_ARGS(fc_saveBackupStartPoint),
						LSN_FORMAT_ARGS(fc_saveBackupEndPoint))));
	}

	/*
	 * 我们是否已经超过了安全的启动点？请注意，如果从备份恢复，minRecoveryPoint已知设置不正确，直到XLOG_BACKUP_END到达，告诉我们正确的minRecoveryPoint。在此之前，我们只知道我们尚未一致。
	 */
	if (!reachedConsistency && !backupEndRequired &&
		minRecoveryPoint <= fc_lastReplayedEndRecPtr)
	{
		/*
		 * 检查XLOG序列是否包含对未初始化页面的任何未解决引用。
		 */
		XLogCheckInvalidPages();

		/*
		 * 检查pg_tblspc是否不包含任何真实目录。数据库/CREATE_*记录的回放可能创建了虚构的表空间目录，这些目录在达到一致性时应该已经被移除。
		 */
		fc_CheckTablespaceDirectory();

		reachedConsistency = true;
		ereport(LOG,
				(errmsg("consistent recovery state reached at %X/%X",
						LSN_FORMAT_ARGS(fc_lastReplayedEndRecPtr))));
	}

	/*
	 * 我们是否有一个有效的起始快照，允许运行查询？如果是这样，我们可以告诉postmaster数据库现在是一致的，从而启用连接。
	 */
	if (standbyState == STANDBY_SNAPSHOT_READY &&
		!LocalHotStandbyActive &&
		reachedConsistency &&
		IsUnderPostmaster)
	{
		SpinLockAcquire(&XLogRecoveryCtl->info_lck);
		XLogRecoveryCtl->SharedHotStandbyActive = true;
		SpinLockRelease(&XLogRecoveryCtl->info_lck);

		LocalHotStandbyActive = true;

		SendPostmasterSignal(PMSIGNAL_BEGIN_HOT_STANDBY);
	}
}

/*
 * 在rm_redo()过程中发生错误时的错误上下文回调。
 */
static void fc_rm_redo_error_callback(void *fc_arg)
{
	XLogReaderState *fc_record = (XLogReaderState *) fc_arg;
	StringInfoData fc_buf;

	initStringInfo(&fc_buf);
	xlog_outdesc(&fc_buf, fc_record);
	fc_xlog_block_info(&fc_buf, fc_record);

	/* 翻译者：%s是WAL记录描述 */
	errcontext("WAL redo at %X/%X for %s",
			   LSN_FORMAT_ARGS(fc_record->ReadRecPtr),
			   fc_buf.data);

	pfree(fc_buf.data);
}

/*
 * 返回一个描述XLogRecord的字符串，包括其标识，后面可选地跟着冒号、空格和进一步的描述。
 */
void xlog_outdesc(StringInfo fc_buf, XLogReaderState *fc_record)
{
	RmgrData	fc_rmgr = GetRmgr(XLogRecGetRmid(fc_record));
	uint8		fc_info = XLogRecGetInfo(fc_record);
	const char *fc_id;

	appendStringInfoString(fc_buf, fc_rmgr.rm_name);
	appendStringInfoChar(fc_buf, '/');

	fc_id = fc_rmgr.rm_identify(fc_info);
	if (fc_id == NULL)
		appendStringInfo(fc_buf, "UNKNOWN (%X): ", fc_info & ~XLR_INFO_MASK);
	else
		appendStringInfo(fc_buf, "%s: ", fc_id);

	fc_rmgr.rm_desc(fc_buf, fc_record);
}

#ifdef WAL_DEBUG

static void fc_xlog_outrec(StringInfo fc_buf, XLogReaderState *fc_record)
{
	appendStringInfo(fc_buf, "prev %X/%X; xid %u",
					 LSN_FORMAT_ARGS(XLogRecGetPrev(fc_record)),
					 XLogRecGetXid(fc_record));

	appendStringInfo(fc_buf, "; len %u",
					 XLogRecGetDataLen(fc_record));

	fc_xlog_block_info(fc_buf, fc_record);
}
#endif							/* WAL_DEBUG */

/*
 * 返回一个字符串，提供有关 XLogRecord 中所有块的信息。
 */
static void fc_xlog_block_info(StringInfo fc_buf, XLogReaderState *fc_record)
{
	int			fc_block_id;

	/* 解码块引用 */
	for (fc_block_id = 0; fc_block_id <= XLogRecMaxBlockId(fc_record); fc_block_id++)
	{
		RelFileNode fc_rnode;
		ForkNumber	fc_forknum;
		BlockNumber fc_blk;

		if (!XLogRecGetBlockTagExtended(fc_record, fc_block_id,
										&fc_rnode, &fc_forknum, &fc_blk, NULL))
			continue;

		if (fc_forknum != MAIN_FORKNUM)
			appendStringInfo(fc_buf, "; blkref #%d: rel %u/%u/%u, fork %u, blk %u",
							 fc_block_id,
							 fc_rnode.spcNode, fc_rnode.dbNode, fc_rnode.relNode,
							 fc_forknum,
							 fc_blk);
		else
			appendStringInfo(fc_buf, "; blkref #%d: rel %u/%u/%u, blk %u",
							 fc_block_id,
							 fc_rnode.spcNode, fc_rnode.dbNode, fc_rnode.relNode,
							 fc_blk);
		if (XLogRecHasBlockImage(fc_record, fc_block_id))
			appendStringInfoString(fc_buf, " FPW");
	}
}


/*
 * 检查在恢复期间切换到新时间线是否可以。
 *
 * 'lsn'是我们即将回放的关机检查点记录的地址。（目前，时间线只能在关机检查点处更改）。
 */
static void fc_checkTimeLineSwitch(XLogRecPtr fc_lsn, TimeLineID fc_newTLI, TimeLineID fc_prevTLI,
					TimeLineID fc_replayTLI)
{
	/* 检查记录是否一致当前（旧）时间线是什么 */
	if (fc_prevTLI != fc_replayTLI)
		ereport(PANIC,
				(errmsg("unexpected previous timeline ID %u (current timeline ID %u) in checkpoint record",
						fc_prevTLI, fc_replayTLI)));

	/*
	 * 新时间线最好在我们期望看到的时间线列表中，按照时间线历史记录。它也不应减少。
	 */
	if (fc_newTLI < fc_replayTLI || !tliInHistory(fc_newTLI, expectedTLEs))
		ereport(PANIC,
				(errmsg("unexpected timeline ID %u (after %u) in checkpoint record",
						fc_newTLI, fc_replayTLI)));

	/*
	 * 如果我们尚未达到最小恢复点，并且即将切换到大于最小恢复点时间线的时间线：麻烦了。在切换到新时间线后，我们将无法在正确的时间线上访问最小恢复点。这可能发生在存档中有一个较新的时间线，该时间线在最小恢复点所处的时间线之前分支，并且您尝试在新时间线上进行PITR。
	 */
	if (!XLogRecPtrIsInvalid(minRecoveryPoint) &&
		fc_lsn < minRecoveryPoint &&
		fc_newTLI > minRecoveryPointTLI)
		ereport(PANIC,
				(errmsg("unexpected timeline ID %u in checkpoint record, before reaching minimum recovery point %X/%X on timeline %u",
						fc_newTLI,
						LSN_FORMAT_ARGS(minRecoveryPoint),
						minRecoveryPointTLI)));

	/* 看起来不错 */
}


/*
 * 从WAL记录中提取时间戳。
 *
 * 如果记录包含时间戳，则返回true，并将时间戳保存在*recordXtime中。如果记录类型没有时间戳，则返回false。目前，只有事务提交/中止记录和恢复点包含时间戳。
 */
static bool fc_getRecordTimestamp(XLogReaderState *fc_record, TimestampTz *fc_recordXtime)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;
	uint8		fc_xact_info = fc_info & XLOG_XACT_OPMASK;
	uint8		fc_rmid = XLogRecGetRmid(fc_record);

	if (fc_rmid == RM_XLOG_ID && fc_info == XLOG_RESTORE_POINT)
	{
		*fc_recordXtime = ((xl_restore_point *) XLogRecGetData(fc_record))->rp_time;
		return true;
	}
	if (fc_rmid == RM_XACT_ID && (fc_xact_info == XLOG_XACT_COMMIT ||
							   fc_xact_info == XLOG_XACT_COMMIT_PREPARED))
	{
		*fc_recordXtime = ((xl_xact_commit *) XLogRecGetData(fc_record))->xact_time;
		return true;
	}
	if (fc_rmid == RM_XACT_ID && (fc_xact_info == XLOG_XACT_ABORT ||
							   fc_xact_info == XLOG_XACT_ABORT_PREPARED))
	{
		*fc_recordXtime = ((xl_xact_abort *) XLogRecGetData(fc_record))->xact_time;
		return true;
	}
	return false;
}

/*
 * 检查当前缓冲页面和存储在WAL记录中的备份页面是否一致。在比较两个页面之前，可以对页面应用掩码以忽略某些区域，例如提示位、pd_lower和pd_upper之间的未使用空间等。此函数应在给定记录的WAL回放完成后调用。
 */
static void fc_verifyBackupPageConsistency(XLogReaderState *fc_record)
{
	RmgrData	fc_rmgr = GetRmgr(XLogRecGetRmid(fc_record));
	RelFileNode fc_rnode;
	ForkNumber	fc_forknum;
	BlockNumber fc_blkno;
	int			fc_block_id;

	/* 没有备份块的记录不需要进行一致性检查。 */
	if (!XLogRecHasAnyBlockRefs(fc_record))
		return;

	Assert((XLogRecGetInfo(fc_record) & XLR_CHECK_CONSISTENCY) != 0);

	for (fc_block_id = 0; fc_block_id <= XLogRecMaxBlockId(fc_record); fc_block_id++)
	{
		Buffer		fc_buf;
		Page		fc_page;

		if (!XLogRecGetBlockTagExtended(fc_record, fc_block_id,
										&fc_rnode, &fc_forknum, &fc_blkno, NULL))
		{
			/*
			 * WAL 记录不包含具有给定 ID 的块引用。
			 * 不做任何操作。
			 */
			continue;
		}

		Assert(XLogRecHasBlockImage(fc_record, fc_block_id));

		if (XLogRecBlockImageApply(fc_record, fc_block_id))
		{
			/*
			 * WAL 记录已经应用了该页面，因此跳过
			 * 一致性检查，因为那样将导致将记录中存储的完整
			 * 页面与其自身进行比较。
			 */
			continue;
		}

		/*
		 * 从当前缓冲区读取内容并将其存储在一个
		 * 临时页面中。
		 */
		fc_buf = XLogReadBufferExtended(fc_rnode, fc_forknum, fc_blkno,
									 RBM_NORMAL_NO_LOG,
									 InvalidBuffer);
		if (!BufferIsValid(fc_buf))
			continue;

		LockBuffer(fc_buf, BUFFER_LOCK_EXCLUSIVE);
		fc_page = BufferGetPage(fc_buf);

		/*
		 * 拍摄一份局部页面的副本，WAL 已应用于该页面，以便在掩码之前有
		 * 一个比较基础...
		 */
		memcpy(replay_image_masked, fc_page, BLCKSZ);

		/* 现在有了副本，这个页面不再需要。 */
		UnlockReleaseBuffer(fc_buf);

		/*
		 * 如果块 LSN 已经在此 WAL 记录之前，我们不能
		 * 期待内容匹配。这可能发生在恢复
		 * 重启时。
		 */
		if (PageGetLSN(replay_image_masked) > fc_record->EndRecPtr)
			continue;

		/*
		 * 从备份副本中读取内容，存储在 WAL 记录中，并
		 * 将其存储在一个临时页面中。这里不需要分配一个新的
		 * 页面，本地缓冲区可以很好地保存其内容，并且掩码
		 * 可以直接作用于其上。
		 */
		if (!RestoreBlockImage(fc_record, fc_block_id, primary_image_masked))
			ereport(ERROR,
					(errcode(ERRCODE_INTERNAL_ERROR),
					 errmsg_internal("%s", fc_record->errormsg_buf)));

		/*
		 * 如果定义了掩码函数，则对主图像和重放
		 * 图像进行掩码处理。
		 */
		if (fc_rmgr.rm_mask != NULL)
		{
			fc_rmgr.rm_mask(replay_image_masked, fc_blkno);
			fc_rmgr.rm_mask(primary_image_masked, fc_blkno);
		}

		/* 是时候比较主图像和重播图像了。 */
		if (memcmp(replay_image_masked, primary_image_masked, BLCKSZ) != 0)
		{
			elog(FATAL,
				 "inconsistent page found, rel %u/%u/%u, forknum %u, blkno %u",
				 fc_rnode.spcNode, fc_rnode.dbNode, fc_rnode.relNode,
				 fc_forknum, fc_blkno);
		}
	}
}

/*
 * 对于时间点恢复，此函数决定我们是否想要
 * 在当前记录之前停止应用 XLOG。
 *
 * 如果我们要停止，则返回 true，否则返回 false。如果停止，则
 * 一些信息会保存在 recoveryStopXid 等中，以便在注释
 * 新时间线的历史文件中使用。
 */
static bool fc_recoveryStopsBefore(XLogReaderState *fc_record)
{
	bool		fc_stopsHere = false;
	uint8		fc_xact_info;
	bool		fc_isCommit;
	TimestampTz fc_recordXtime = 0;
	TransactionId fc_recordXid;

	/*
	 * 当不在归档恢复时忽略恢复目标设置（意味着
	 * 我们处于崩溃恢复中）。
	 */
	if (!ArchiveRecoveryRequested)
		return false;

	/* 检查我们是否应该在达到一致性时立即停止 */
	if (recoveryTarget == RECOVERY_TARGET_IMMEDIATE && reachedConsistency)
	{
		ereport(LOG,
				(errmsg("recovery stopping after reaching consistency")));

		recoveryStopAfter = false;
		recoveryStopXid = InvalidTransactionId;
		recoveryStopLSN = InvalidXLogRecPtr;
		recoveryStopTime = 0;
		recoveryStopName[0] = '\0';
		return true;
	}

	/* 检查是否已达到目标 LSN */
	if (recoveryTarget == RECOVERY_TARGET_LSN &&
		!recoveryTargetInclusive &&
		fc_record->ReadRecPtr >= recoveryTargetLSN)
	{
		recoveryStopAfter = false;
		recoveryStopXid = InvalidTransactionId;
		recoveryStopLSN = fc_record->ReadRecPtr;
		recoveryStopTime = 0;
		recoveryStopName[0] = '\0';
		ereport(LOG,
				(errmsg("recovery stopping before WAL location (LSN) \"%X/%X\"",
						LSN_FORMAT_ARGS(recoveryStopLSN))));
		return true;
	}

	/* 否则我们只考虑在 COMMIT 或 ABORT 记录之前停止。 */
	if (XLogRecGetRmid(fc_record) != RM_XACT_ID)
		return false;

	fc_xact_info = XLogRecGetInfo(fc_record) & XLOG_XACT_OPMASK;

	if (fc_xact_info == XLOG_XACT_COMMIT)
	{
		fc_isCommit = true;
		fc_recordXid = XLogRecGetXid(fc_record);
	}
	else if (fc_xact_info == XLOG_XACT_COMMIT_PREPARED)
	{
		xl_xact_commit *fc_xlrec = (xl_xact_commit *) XLogRecGetData(fc_record);
		xl_xact_parsed_commit fc_parsed;

		fc_isCommit = true;
		ParseCommitRecord(XLogRecGetInfo(fc_record),
						  fc_xlrec,
						  &fc_parsed);
		fc_recordXid = fc_parsed.twophase_xid;
	}
	else if (fc_xact_info == XLOG_XACT_ABORT)
	{
		fc_isCommit = false;
		fc_recordXid = XLogRecGetXid(fc_record);
	}
	else if (fc_xact_info == XLOG_XACT_ABORT_PREPARED)
	{
		xl_xact_abort *fc_xlrec = (xl_xact_abort *) XLogRecGetData(fc_record);
		xl_xact_parsed_abort fc_parsed;

		fc_isCommit = false;
		ParseAbortRecord(XLogRecGetInfo(fc_record),
						 fc_xlrec,
						 &fc_parsed);
		fc_recordXid = fc_parsed.twophase_xid;
	}
	else
		return false;

	if (recoveryTarget == RECOVERY_TARGET_XID && !recoveryTargetInclusive)
	{
		/*
		 * 具有此确切 transactionid 的事务结束记录只能有一个
		 *
		 * 在测试一个 xid 时，我们必须仅测试相等性，因为
		 * 事务是按开始顺序编号的，而不是按完成顺序编号的。一个较高编号的 xid 会在
		 * 你前面完成大约 50% 的时间...
		 */
		fc_stopsHere = (fc_recordXid == recoveryTargetXid);
	}

	/*
	 * 注意：无论 recoveryTarget 设置如何，我们都必须获取 recordXtime。
	 * 我们不期望 getRecordTimestamp 会失败，因为我们已经知道
	 * 这是一个提交或中止记录；但仍然测试其结果。
	 */
	if (fc_getRecordTimestamp(fc_record, &fc_recordXtime) &&
		recoveryTarget == RECOVERY_TARGET_TIME)
	{
		/*
		 * 可能有许多事务共享相同的提交时间，因此
		 * 如果我们是包容性的，我们将在最后一个之后停止，或在第一个
		 * 之前停止，如果我们是排他的。
		 */
		if (recoveryTargetInclusive)
			fc_stopsHere = (fc_recordXtime > recoveryTargetTime);
		else
			fc_stopsHere = (fc_recordXtime >= recoveryTargetTime);
	}

	if (fc_stopsHere)
	{
		recoveryStopAfter = false;
		recoveryStopXid = fc_recordXid;
		recoveryStopTime = fc_recordXtime;
		recoveryStopLSN = InvalidXLogRecPtr;
		recoveryStopName[0] = '\0';

		if (fc_isCommit)
		{
			ereport(LOG,
					(errmsg("recovery stopping before commit of transaction %u, time %s",
							recoveryStopXid,
							timestamptz_to_str(recoveryStopTime))));
		}
		else
		{
			ereport(LOG,
					(errmsg("recovery stopping before abort of transaction %u, time %s",
							recoveryStopXid,
							timestamptz_to_str(recoveryStopTime))));
		}
	}

	return fc_stopsHere;
}

/*
 * 与 recoveryStopsBefore 相同，但在应用记录后调用。
 *
 * 我们还跟踪 XLogRecoveryCtl->recoveryLastXTime 中最新应用的 COMMIT/ABORT
 * 记录的时间戳。
 */
static bool fc_recoveryStopsAfter(XLogReaderState *fc_record)
{
	uint8		fc_info;
	uint8		fc_xact_info;
	uint8		fc_rmid;
	TimestampTz fc_recordXtime = 0;

	/*
	 * 当不在归档恢复时忽略恢复目标设置（意味着
	 * 我们处于崩溃恢复中）。
	 */
	if (!ArchiveRecoveryRequested)
		return false;

	fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;
	fc_rmid = XLogRecGetRmid(fc_record);

	/*
	 * 可能存在多个共享相同名称的还原点；我们停止在第一个还原点。
	 */
	if (recoveryTarget == RECOVERY_TARGET_NAME &&
		fc_rmid == RM_XLOG_ID && fc_info == XLOG_RESTORE_POINT)
	{
		xl_restore_point *fc_recordRestorePointData;

		fc_recordRestorePointData = (xl_restore_point *) XLogRecGetData(fc_record);

		if (strcmp(fc_recordRestorePointData->rp_name, recoveryTargetName) == 0)
		{
			recoveryStopAfter = true;
			recoveryStopXid = InvalidTransactionId;
			recoveryStopLSN = InvalidXLogRecPtr;
			(void) fc_getRecordTimestamp(fc_record, &recoveryStopTime);
			strlcpy(recoveryStopName, fc_recordRestorePointData->rp_name, MAXFNAMELEN);

			ereport(LOG,
					(errmsg("recovery stopping at restore point \"%s\", time %s",
							recoveryStopName,
							timestamptz_to_str(recoveryStopTime))));
			return true;
		}
	}

	/* 检查目标 LSN 是否已达到 */
	if (recoveryTarget == RECOVERY_TARGET_LSN &&
		recoveryTargetInclusive &&
		fc_record->ReadRecPtr >= recoveryTargetLSN)
	{
		recoveryStopAfter = true;
		recoveryStopXid = InvalidTransactionId;
		recoveryStopLSN = fc_record->ReadRecPtr;
		recoveryStopTime = 0;
		recoveryStopName[0] = '\0';
		ereport(LOG,
				(errmsg("recovery stopping after WAL location (LSN) \"%X/%X\"",
						LSN_FORMAT_ARGS(recoveryStopLSN))));
		return true;
	}

	if (fc_rmid != RM_XACT_ID)
		return false;

	fc_xact_info = fc_info & XLOG_XACT_OPMASK;

	if (fc_xact_info == XLOG_XACT_COMMIT ||
		fc_xact_info == XLOG_XACT_COMMIT_PREPARED ||
		fc_xact_info == XLOG_XACT_ABORT ||
		fc_xact_info == XLOG_XACT_ABORT_PREPARED)
	{
		TransactionId fc_recordXid;

		/* 更新最后应用的事务时间戳 */
		if (fc_getRecordTimestamp(fc_record, &fc_recordXtime))
			fc_SetLatestXTime(fc_recordXtime);

		/* 提取已提交/中止事务的 XID */
		if (fc_xact_info == XLOG_XACT_COMMIT_PREPARED)
		{
			xl_xact_commit *fc_xlrec = (xl_xact_commit *) XLogRecGetData(fc_record);
			xl_xact_parsed_commit fc_parsed;

			ParseCommitRecord(XLogRecGetInfo(fc_record),
							  fc_xlrec,
							  &fc_parsed);
			fc_recordXid = fc_parsed.twophase_xid;
		}
		else if (fc_xact_info == XLOG_XACT_ABORT_PREPARED)
		{
			xl_xact_abort *fc_xlrec = (xl_xact_abort *) XLogRecGetData(fc_record);
			xl_xact_parsed_abort fc_parsed;

			ParseAbortRecord(XLogRecGetInfo(fc_record),
							 fc_xlrec,
							 &fc_parsed);
			fc_recordXid = fc_parsed.twophase_xid;
		}
		else
			fc_recordXid = XLogRecGetXid(fc_record);

		/*
		 * 具有此确切 transactionid 的事务结束记录只能有一个
		 *
		 * 在测试一个 xid 时，我们必须仅测试相等性，因为
		 * 事务是按开始顺序编号的，而不是按完成顺序编号的。一个较高编号的 xid 会在
		 * 你前面完成大约 50% 的时间...
		 */
		if (recoveryTarget == RECOVERY_TARGET_XID && recoveryTargetInclusive &&
			fc_recordXid == recoveryTargetXid)
		{
			recoveryStopAfter = true;
			recoveryStopXid = fc_recordXid;
			recoveryStopTime = fc_recordXtime;
			recoveryStopLSN = InvalidXLogRecPtr;
			recoveryStopName[0] = '\0';

			if (fc_xact_info == XLOG_XACT_COMMIT ||
				fc_xact_info == XLOG_XACT_COMMIT_PREPARED)
			{
				ereport(LOG,
						(errmsg("recovery stopping after commit of transaction %u, time %s",
								recoveryStopXid,
								timestamptz_to_str(recoveryStopTime))));
			}
			else if (fc_xact_info == XLOG_XACT_ABORT ||
					 fc_xact_info == XLOG_XACT_ABORT_PREPARED)
			{
				ereport(LOG,
						(errmsg("recovery stopping after abort of transaction %u, time %s",
								recoveryStopXid,
								timestamptz_to_str(recoveryStopTime))));
			}
			return true;
		}
	}

	/* 检查我们是否应该在达到一致性时立即停止 */
	if (recoveryTarget == RECOVERY_TARGET_IMMEDIATE && reachedConsistency)
	{
		ereport(LOG,
				(errmsg("recovery stopping after reaching consistency")));

		recoveryStopAfter = true;
		recoveryStopXid = InvalidTransactionId;
		recoveryStopTime = 0;
		recoveryStopLSN = InvalidXLogRecPtr;
		recoveryStopName[0] = '\0';
		return true;
	}

	return false;
}

/*
 * 创建历史文件的注释以解释时间线为什么以及在哪里发生了变化。
 */
static char * fc_getRecoveryStopReason(void)
{
	char		fc_reason[200];

	if (recoveryTarget == RECOVERY_TARGET_XID)
		snprintf(fc_reason, sizeof(fc_reason),
				 "%s transaction %u",
				 recoveryStopAfter ? "after" : "before",
				 recoveryStopXid);
	else if (recoveryTarget == RECOVERY_TARGET_TIME)
		snprintf(fc_reason, sizeof(fc_reason),
				 "%s %s\n",
				 recoveryStopAfter ? "after" : "before",
				 timestamptz_to_str(recoveryStopTime));
	else if (recoveryTarget == RECOVERY_TARGET_LSN)
		snprintf(fc_reason, sizeof(fc_reason),
				 "%s LSN %X/%X\n",
				 recoveryStopAfter ? "after" : "before",
				 LSN_FORMAT_ARGS(recoveryStopLSN));
	else if (recoveryTarget == RECOVERY_TARGET_NAME)
		snprintf(fc_reason, sizeof(fc_reason),
				 "at restore point \"%s\"",
				 recoveryStopName);
	else if (recoveryTarget == RECOVERY_TARGET_IMMEDIATE)
		snprintf(fc_reason, sizeof(fc_reason), "reached consistency");
	else
		snprintf(fc_reason, sizeof(fc_reason), "no recovery target specified");

	return pstrdup(fc_reason);
}

/*
 * 等待直到共享的 recoveryPauseState 被设置为 RECOVERY_NOT_PAUSED。
 *
 * 如果恢复目标已达到，则 endOfRecovery 为真；如果由于
 * recovery_target_action=pause 而使暂停状态从恢复结束时开始，则为假，反之亦然。
 */
static void fc_recoveryPausesHere(bool fc_endOfRecovery)
{
	/* 除非用户可以连接，否则不要暂停！ */
	if (!LocalHotStandbyActive)
		return;

	/* 在触发备用提升后不要暂停 */
	if (LocalPromoteIsTriggered)
		return;

	if (fc_endOfRecovery)
		ereport(LOG,
				(errmsg("pausing at the end of recovery"),
				 errhint("Execute pg_wal_replay_resume() to promote.")));
	else
		ereport(LOG,
				(errmsg("recovery has paused"),
				 errhint("Execute pg_wal_replay_resume() to continue.")));

	/* 循环直到 recoveryPauseState 被设置为 RECOVERY_NOT_PAUSED */
	while (GetRecoveryPauseState() != RECOVERY_NOT_PAUSED)
	{
		HandleStartupProcInterrupts();
		if (fc_CheckForStandbyTrigger())
			return;

		/*
		 * 如果请求恢复暂停，则将其设置为暂停。在循环中，用户可能会恢复并再次暂停，因此每次都要设置此变量。
		 */
		fc_ConfirmRecoveryPaused();

		/*
		 * 我们在一个条件变量上等待，当暂停结束时会唤醒我们，
		 * 但我们使用超时，这样我们也可以定期检查上面的退出条件。
		 */
		ConditionVariableTimedSleep(&XLogRecoveryCtl->recoveryNotPausedCV, 1000,
									WAIT_EVENT_RECOVERY_PAUSE);
	}
	ConditionVariableCancelSleep();
}

/*
 * 当 recovery_min_apply_delay 被设置时，我们等待足够长的时间以确保
 * 某些记录类型至少在主服务器之后延迟该间隔的时间。
 *
 * 如果我们等待，则返回真。
 *
 * 注意，延迟是根据 WAL 记录日志时间和备用上的当前时间计算的。我们更愿意跟踪此
 * 备用何时接收到每个 WAL 记录，这将允许更一致的方法，并且不受时间同步问题的影响，
 * 但是这需要更多的努力和复杂性，而在可用性方面的实际收益不大。
 */
static bool fc_recoveryApplyDelay(XLogReaderState *fc_record)
{
	uint8		fc_xact_info;
	TimestampTz fc_xtime;
	TimestampTz fc_delayUntil;
	long		fc_msecs;

	/* 如果未配置延迟，则无事可做 */
	if (recovery_min_apply_delay <= 0)
		return false;

	/* 在尚未一致的数据库上不应用延迟 */
	if (!reachedConsistency)
		return false;

	/* 如果请求崩溃恢复，则无事可做 */
	if (!ArchiveRecoveryRequested)
		return false;

	/*
	 * 这是一个 COMMIT 记录吗？
	 *
	 * 我们有意选择不延迟中止，因为它们对 MVCC 没有影响。
	 * 我们已经允许重放没有时间戳的记录，
	 * 因此，对于由早期冲突在备用上引起的问题已经存在了机会。
	 */
	if (XLogRecGetRmid(fc_record) != RM_XACT_ID)
		return false;

	fc_xact_info = XLogRecGetInfo(fc_record) & XLOG_XACT_OPMASK;

	if (fc_xact_info != XLOG_XACT_COMMIT &&
		fc_xact_info != XLOG_XACT_COMMIT_PREPARED)
		return false;

	if (!fc_getRecordTimestamp(fc_record, &fc_xtime))
		return false;

	fc_delayUntil = TimestampTzPlusMilliseconds(fc_xtime, recovery_min_apply_delay);

	/*
	 * 如果已经超过应用此记录的时间，则退出而不设定闩锁
	 */
	fc_msecs = TimestampDifferenceMilliseconds(GetCurrentTimestamp(), fc_delayUntil);
	if (fc_msecs <= 0)
		return false;

	while (true)
	{
		ResetLatch(&XLogRecoveryCtl->recoveryWakeupLatch);

		/*
		 * 这可能会改变 recovery_min_apply_delay 或触发文件的位置。
		 */
		HandleStartupProcInterrupts();

		if (fc_CheckForStandbyTrigger())
			break;

		/*
		 * 重新计算 delayUntil，因为在这个循环中等待时 recovery_min_apply_delay 可能已经改变。
		 */
		fc_delayUntil = TimestampTzPlusMilliseconds(fc_xtime, recovery_min_apply_delay);

		/*
		 * 等待 GetCurrentTimestamp() 和 delayUntil 之间的差异。
		 */
		fc_msecs = TimestampDifferenceMilliseconds(GetCurrentTimestamp(),
												fc_delayUntil);

		if (fc_msecs <= 0)
			break;

		elog(DEBUG2, "recovery apply delay %ld milliseconds", fc_msecs);

		(void) WaitLatch(&XLogRecoveryCtl->recoveryWakeupLatch,
						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
						 fc_msecs,
						 WAIT_EVENT_RECOVERY_APPLY_DELAY);
	}
	return true;
}

/*
 * 获取恢复暂停请求的当前状态。
 */
RecoveryPauseState
GetRecoveryPauseState(void)
{
	RecoveryPauseState fc_state;

	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	fc_state = XLogRecoveryCtl->recoveryPauseState;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	return fc_state;
}

/*
 * 设置恢复暂停状态。
 *
 * 如果请求恢复暂停，则将恢复暂停状态设置为
 * “暂停请求”，如果它尚未处于“暂停”状态。否则，将其设置为
 * “未暂停”以恢复恢复。恢复暂停将通过 ConfirmRecoveryPaused 确认。
 */
void SetRecoveryPause(bool fc_recoveryPause)
{
	SpinLockAcquire(&XLogRecoveryCtl->info_lck);

	if (!fc_recoveryPause)
		XLogRecoveryCtl->recoveryPauseState = RECOVERY_NOT_PAUSED;
	else if (XLogRecoveryCtl->recoveryPauseState == RECOVERY_NOT_PAUSED)
		XLogRecoveryCtl->recoveryPauseState = RECOVERY_PAUSE_REQUESTED;

	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	if (!fc_recoveryPause)
		ConditionVariableBroadcast(&XLogRecoveryCtl->recoveryNotPausedCV);
}

/*
 * 通过将恢复暂停状态设置为
 * RECOVERY_PAUSED 来确认恢复暂停。
 */
static void fc_ConfirmRecoveryPaused(void)
{
	/* 如果请求恢复暂停，则将其设置为暂停 */
	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	if (XLogRecoveryCtl->recoveryPauseState == RECOVERY_PAUSE_REQUESTED)
		XLogRecoveryCtl->recoveryPauseState = RECOVERY_PAUSED;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);
}


/*
 * 尝试读取下一个 XLOG 记录。
 *
 * 在第一次调用之前，读取器需要通过调用 XLogPrefetcherBeginRead()
 * 将位置设置到第一个记录。
 *
 * 如果没有可用的有效记录，返回 NULL，或者如果 emode 为 PANIC，则失败。
 * （emode 必须是 PANIC 或 LOG）。在备用模式下，重试直到有有效的
 * 记录可用。
 */
static XLogRecord * fc_ReadRecord(XLogPrefetcher *fc_xlogprefetcher, int fc_emode,
		   bool fc_fetching_ckpt, TimeLineID fc_replayTLI)
{
	XLogRecord *fc_record;
	XLogReaderState *fc_xlogreader = XLogPrefetcherGetReader(fc_xlogprefetcher);
	XLogPageReadPrivate *fc_private = (XLogPageReadPrivate *) fc_xlogreader->private_data;

	/* 将参数传递给 XLogPageRead */
	fc_private->fetching_ckpt = fc_fetching_ckpt;
	fc_private->emode = fc_emode;
	fc_private->randAccess = (fc_xlogreader->ReadRecPtr == InvalidXLogRecPtr);
	fc_private->replayTLI = fc_replayTLI;

	/* 这是读取此页面的第一次尝试。 */
	lastSourceFailed = false;

	for (;;)
	{
		char	   *fc_errormsg;

		fc_record = XLogPrefetcherReadRecord(fc_xlogprefetcher, &fc_errormsg);
		if (fc_record == NULL)
		{
			/*
			 * 当我们发现 WAL 以一个不完整的记录结束时，保持跟踪
			 * 该记录。恢复完成后，我们将写入一条记录来
			 * 指示下游 WAL 读取者忽略该部分。
			 *
			 * 然而，当 ArchiveRecoveryRequested = true 时，我们将在恢复结束时
			 * 切换到一个新的时间线。我们只会
			 * 将 WAL 复制到新时间线，直到最后一个
			 * 完整记录的结束，因此如果我们这样做，就会在错误的位置
			 * 创建一个覆盖 contrecord，打破一切。
			 */
			if (!ArchiveRecoveryRequested &&
				!XLogRecPtrIsInvalid(fc_xlogreader->abortedRecPtr))
			{
				abortedRecPtr = fc_xlogreader->abortedRecPtr;
				missingContrecPtr = fc_xlogreader->missingContrecPtr;
			}

			if (readFile >= 0)
			{
				close(readFile);
				readFile = -1;
			}

			/*
			 * 当 XLogPageRead() 失败时，我们只会最终来到这里
			 * - 在这种情况下，我们已经记录了一些内容。在
			 * StandbyMode 中，只有在我们已被触发时才会发生这种情况，
			 * 所以在这种情况下我们不应该再循环了。
			 */
			if (fc_errormsg)
				ereport(fc_emode_for_corrupt_record(fc_emode, fc_xlogreader->EndRecPtr),
						(errmsg_internal("%s", fc_errormsg) /* 已经翻译 */ ));
		}

		/*
		 * 检查页面 TLI 是否是预期值之一。
		 */
		else if (!tliInHistory(fc_xlogreader->latestPageTLI, expectedTLEs))
		{
			char		fc_fname[MAXFNAMELEN];
			XLogSegNo	fc_segno;
			int32		fc_offset;

			XLByteToSeg(fc_xlogreader->latestPagePtr, fc_segno, wal_segment_size);
			fc_offset = XLogSegmentOffset(fc_xlogreader->latestPagePtr,
									   wal_segment_size);
			XLogFileName(fc_fname, fc_xlogreader->seg.ws_tli, fc_segno,
						 wal_segment_size);
			ereport(fc_emode_for_corrupt_record(fc_emode, fc_xlogreader->EndRecPtr),
					(errmsg("unexpected timeline ID %u in log segment %s, offset %u",
							fc_xlogreader->latestPageTLI,
							fc_fname,
							fc_offset)));
			fc_record = NULL;
		}

		if (fc_record)
		{
			/* 很好，得到了一个记录 */
			return fc_record;
		}
		else
		{
			/* 该来源没有可用的有效记录 */
			lastSourceFailed = true;

			/*
			 * 如果请求了归档恢复，但我们仍在进行
			 * 崩溃恢复，则切换到归档恢复并使用
			 * 离线归档重试。我们现在已经重放了 pg_wal 中所有有效的 WAL，
			 * 因此我们现在假定是一致的。
			 *
			 * 我们要求 pg_wal 中至少存在一些有效的 WAL，
			 * 然而（！fetching_ckpt）。我们可以使用
			 * 来自归档的 WAL 进行恢复，即使 pg_wal 完全为空，但
			 * 我们将不知道要重放多远才能达到
			 * 一致性。因此，为了安全起见，放弃吧。
			 */
			if (!InArchiveRecovery && ArchiveRecoveryRequested &&
				!fc_fetching_ckpt)
			{
				ereport(DEBUG1,
						(errmsg_internal("reached end of WAL in pg_wal, entering archive recovery")));
				InArchiveRecovery = true;
				if (StandbyModeRequested)
					fc_EnableStandbyMode();

				SwitchIntoArchiveRecovery(fc_xlogreader->EndRecPtr, fc_replayTLI);
				minRecoveryPoint = fc_xlogreader->EndRecPtr;
				minRecoveryPointTLI = fc_replayTLI;

				fc_CheckRecoveryConsistency();

				/*
				 * 在我们重试之前，重置 lastSourceFailed 和 currentSource
				 * 以便我们将检查归档。
				 */
				lastSourceFailed = false;
				currentSource = XLOG_FROM_ANY;

				continue;
			}

			/* 在备用模式下，循环返回以重试。否则，放弃。 */
			if (StandbyMode && !fc_CheckForStandbyTrigger())
				continue;
			else
				return NULL;
		}
	}
}

/*
 * 读取包含 RecPtr 的 XLOG 页面到 readBuf（如果尚未读取）。
 * 如果页面成功读取，则返回读取的字节数；如果发生错误，则返回
 * XLREAD_FAIL。当发生错误时，会报告错误，但只有当错误未被
 * 先前报告时才会进行报告。
 *
 * 在预取时，可能会设置 xlogreader->nonblocking。在这种情况下，
 * 如果我们必须等待更多 WAL，则返回 XLREAD_WOULDBLOCK。
 *
 * 负责根据需要从归档中恢复文件，以及在待机模式下等待请求的
 * WAL 记录到达。
 *
 * 'emode' 指定在归档恢复或在找到触发文件的待机模式中，用于报告
 * “未找到文件”或“WAL 结束”情况的日志级别。如果设置为
 * WARNING 或更低，XLogPageRead() 在这些情况下返回 XLREAD_FAIL，
 * 在较高的日志级别，ereport() 将不会返回。
 *
 * 在待机模式下，如果在成功返回 XLogPageRead() 后，调用者发现
 * 感兴趣的记录损坏，则应该根据 emode_for_corrupt_record()
 * 确定的级别报告错误，然后设置 lastSourceFailed，并使用
 * 相同的参数再次调用 XLogPageRead()。这允许 XLogPageRead()
 * 尝试从其他源获取记录，或者睡眠并重试。
 */
static int fc_XLogPageRead(XLogReaderState *fc_xlogreader, XLogRecPtr fc_targetPagePtr, int fc_reqLen,
			 XLogRecPtr fc_targetRecPtr, char *fc_readBuf)
{
	XLogPageReadPrivate *fc_private =
	(XLogPageReadPrivate *) fc_xlogreader->private_data;
	int			fc_emode = fc_private->emode;
	uint32		fc_targetPageOff;
	XLogSegNo	fc_targetSegNo PG_USED_FOR_ASSERTS_ONLY;
	int			fc_r;

	XLByteToSeg(fc_targetPagePtr, fc_targetSegNo, wal_segment_size);
	fc_targetPageOff = XLogSegmentOffset(fc_targetPagePtr, wal_segment_size);

	/*
	 * 查看是否需要切换到新的段，因为请求的记录不在当前打开的段中。
	 */
	if (readFile >= 0 &&
		!XLByteInSeg(fc_targetPagePtr, readSegNo, wal_segment_size))
	{
		/*
		 * 如果我们自上一个重启点以来重放了过多的 xlog，则请求
		 * 一个重启点。
		 */
		if (ArchiveRecoveryRequested && IsUnderPostmaster)
		{
			if (XLogCheckpointNeeded(readSegNo))
			{
				(void) GetRedoRecPtr();
				if (XLogCheckpointNeeded(readSegNo))
					RequestCheckpoint(CHECKPOINT_CAUSE_XLOG);
			}
		}

		close(readFile);
		readFile = -1;
		readSource = XLOG_FROM_ANY;
	}

	XLByteToSeg(fc_targetPagePtr, readSegNo, wal_segment_size);

retry:
	/* 查看是否需要检索更多数据 */
	if (readFile < 0 ||
		(readSource == XLOG_FROM_STREAM &&
		 flushedUpto < fc_targetPagePtr + fc_reqLen))
	{
		if (readFile >= 0 &&
			fc_xlogreader->nonblocking &&
			readSource == XLOG_FROM_STREAM &&
			flushedUpto < fc_targetPagePtr + fc_reqLen)
			return XLREAD_WOULDBLOCK;

		switch (fc_WaitForWALToBecomeAvailable(fc_targetPagePtr + fc_reqLen,
											fc_private->randAccess,
											fc_private->fetching_ckpt,
											fc_targetRecPtr,
											fc_private->replayTLI,
											fc_xlogreader->EndRecPtr,
											fc_xlogreader->nonblocking))
		{
			case XLREAD_WOULDBLOCK:
				return XLREAD_WOULDBLOCK;
			case XLREAD_FAIL:
				if (readFile >= 0)
					close(readFile);
				readFile = -1;
				readLen = 0;
				readSource = XLOG_FROM_ANY;
				return XLREAD_FAIL;
			case XLREAD_SUCCESS:
				break;
		}
	}

	/*
	 * 此时，我们已经打开了正确的段，如果我们正在流式传输，我们
	 * 知道请求的记录就在其中。
	 */
	Assert(readFile != -1);

	/*
	 * 如果当前段是从主节点流式传输的，请计算我们已经接收到的
	 * 当前页面的数量。我们知道请求的记录已被接收，但这对
	 * 未来的调用很有帮助，以便在此函数的顶部快速退出。
	 */
	if (readSource == XLOG_FROM_STREAM)
	{
		if (((fc_targetPagePtr) / XLOG_BLCKSZ) != (flushedUpto / XLOG_BLCKSZ))
			readLen = XLOG_BLCKSZ;
		else
			readLen = XLogSegmentOffset(flushedUpto, wal_segment_size) -
				fc_targetPageOff;
	}
	else
		readLen = XLOG_BLCKSZ;

	/* 读取请求的页面 */
	readOff = fc_targetPageOff;

	pgstat_report_wait_start(WAIT_EVENT_WAL_READ);
	fc_r = pg_pread(readFile, fc_readBuf, XLOG_BLCKSZ, (off_t) readOff);
	if (fc_r != XLOG_BLCKSZ)
	{
		char		fc_fname[MAXFNAMELEN];
		int			fc_save_errno = errno;

		pgstat_report_wait_end();
		XLogFileName(fc_fname, curFileTLI, readSegNo, wal_segment_size);
		if (fc_r < 0)
		{
			errno = fc_save_errno;
			ereport(fc_emode_for_corrupt_record(fc_emode, fc_targetPagePtr + fc_reqLen),
					(errcode_for_file_access(),
					 errmsg("could not read from log segment %s, offset %u: %m",
							fc_fname, readOff)));
		}
		else
			ereport(fc_emode_for_corrupt_record(fc_emode, fc_targetPagePtr + fc_reqLen),
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("could not read from log segment %s, offset %u: read %d of %zu",
							fc_fname, readOff, fc_r, (Size) XLOG_BLCKSZ)));
		goto next_record_is_invalid;
	}
	pgstat_report_wait_end();

	Assert(fc_targetSegNo == readSegNo);
	Assert(fc_targetPageOff == readOff);
	Assert(fc_reqLen <= readLen);

	fc_xlogreader->seg.ws_tli = curFileTLI;

	/*
	 * 立即检查页面头，以便如果无效可以立即重试。这看起来可能没有必要，因为 ReadPageInternal()
	 * 无论如何会验证页面头，并将失败传递给 ReadRecord()，后者会重试。然而，有一个特殊情况涉及续写记录，如果一条记录被拆分到两个页面之间，以至于
	 * 我们需要从两个不同的源中读取这两个页面，这跨越了两个 WAL 段。
	 *
	 * 第一个页面仅在 pg_wal 本地可用，因为它已经在主节点上被回收。然而，第二个页面在 pg_wal 中并不存在，我们应该从主节点流式读取它。有一个回收的 WAL 段存在于 pg_wal 中，里面是垃圾内容。
	 * 我们会从本地 WAL 段读取第一个页面，但在读取第二个页面时，我们会读取错误的、回收过的 WAL
	 * 段。如果我们没有在这里捕捉到该情况，我们将永远无法恢复，因为 ReadRecord() 会从头开始重试读取整个记录。
	 *
	 * 当然，这只捕捉页面头中的错误，这正是回收 WAL 段情况下发生的情况。其他类型的错误或损坏仍然存在同样的问题。但这至少解决了
	 * 常见情况，这可能在正常操作中发生。
	 *
	 * 验证页面头的成本足够低，因此从性能角度来看，进行两次验证不会成问题。
	 *
	 * 当不在待机模式时，无效的页面头应导致恢复结束，而不是重试读取页面，因此我们不需要在重试时在这里验证页面头。
	 * 相反，ReadPageInternal() 负责验证。
	 */
	if (StandbyMode &&
		(fc_targetPagePtr % wal_segment_size) == 0 &&
		!XLogReaderValidatePageHeader(fc_xlogreader, fc_targetPagePtr, fc_readBuf))
	{
		/*
		 * 立即发出这个错误，然后立即重试这个页面。使用 errmsg_internal() 因为消息已经被翻译。
		 */
		if (fc_xlogreader->errormsg_buf[0])
			ereport(fc_emode_for_corrupt_record(fc_emode, fc_xlogreader->EndRecPtr),
					(errmsg_internal("%s", fc_xlogreader->errormsg_buf)));

		/* 重置任何可能由 XLogReaderValidatePageHeader() 设置的错误 */
		XLogReaderResetError(fc_xlogreader);
		goto next_record_is_invalid;
	}

	return readLen;

next_record_is_invalid:

	/*
	 * 如果我们在预读，快速放弃。重试和错误报告将在恢复赶上这个点时由后续读取处理。
	 */
	if (fc_xlogreader->nonblocking)
		return XLREAD_WOULDBLOCK;

	lastSourceFailed = true;

	if (readFile >= 0)
		close(readFile);
	readFile = -1;
	readLen = 0;
	readSource = XLOG_FROM_ANY;

	/* 在待机模式下，继续尝试 */
	if (StandbyMode)
		goto retry;
	else
		return XLREAD_FAIL;
}

/*
 * 打开包含WAL位置'RecPtr'的WAL段。
 *
 * 可以通过restore_command获取该段，或者通过walreceiver流式传输记录来获取，或它已经存在于pg_wal中。检查pg_wal主要是为了崩溃恢复，但在备用模式下也会轮询，以防有人直接将新段复制到pg_wal。不过，这并没有文档说明或推荐。
 *
 * 如果'fetching_ckpt'为真，则我们正在获取检查点记录，并应准备从RedoStartLSN开始读取WAL。
 *
 * 'RecPtr'可能并不指向我们感兴趣记录的开始，它也可能指向页面或段头。在这种情况下，'tliRecPtr'是我们感兴趣的WAL记录的位置。它用于决定从哪个时间线流式传输请求的WAL。
 *
 * 'replayLSN'是当前重放的LSN，因此如果我们扫描新的时间线，我们可以拒绝切换到在此点之前分叉的时间线。
 *
 * 如果记录不可立即获取，则如果我们不在备用模式下，函数返回false。在备用模式下，等待其可用。
 *
 * 当请求的记录变得可用时，函数打开包含该记录的文件（如果尚未打开），并返回XLREAD_SUCCESS。当用户触发备用模式结束，并且没有更多WAL可用时，返回XLREAD_FAIL。
 *
 * 如果nonblocking为真，则如果我们无法满足请求，立即放弃，返回XLREAD_WOULDBLOCK，而不是等待。
 */
static XLogPageReadResult
fc_WaitForWALToBecomeAvailable(XLogRecPtr fc_RecPtr, bool fc_randAccess,
							bool fc_fetching_ckpt, XLogRecPtr fc_tliRecPtr,
							TimeLineID fc_replayTLI, XLogRecPtr fc_replayLSN,
							bool fc_nonblocking)
{
	static TimestampTz fc_last_fail_time = 0;
	TimestampTz fc_now;
	bool		fc_streaming_reply_sent = false;

	/*-------
	 * 备用模式通过状态机实现：
	 *
	 * 1. 从归档或pg_wal读取（XLOG_FROM_ARCHIVE），或者仅从pg_wal读取（XLOG_FROM_PG_WAL）
	 * 2. 检查触发文件
	 * 3. 通过walreceiver从主服务器读取（XLOG_FROM_STREAM）
	 * 4. 重新扫描时间线
	 * 5. 睡眠wal_retrieve_retry_interval毫秒，并循环回1。
	 *
	 * 从当前源读取失败会将状态机推进到下一个状态。
	 *
	 * 'currentSource'表示当前状态。对于"检查触发器"、"重新扫描时间线"和"休眠"状态，没有currentSource值，这些操作是在从上一个源读取失败时进行的，作为推进到下一个状态的一部分。
	 *
	 * 如果在从流中读取WAL时关闭备用模式，我们将转到XLOG_FROM_ARCHIVE并重置lastSourceFailed，以强制从归档或pg_wal中获取文件（例如恢复结束时所需的时间线历史文件）。我们不需要在这里终止WAL接收器，因为在恢复结束时备用模式关闭时，它已经停止。
	 *-------
	 */
	if (!InArchiveRecovery)
		currentSource = XLOG_FROM_PG_WAL;
	else if (currentSource == XLOG_FROM_ANY ||
			 (!StandbyMode && currentSource == XLOG_FROM_STREAM))
	{
		lastSourceFailed = false;
		currentSource = XLOG_FROM_ARCHIVE;
	}

	for (;;)
	{
		XLogSource	fc_oldSource = currentSource;
		bool		fc_startWalReceiver = false;

		/*
		 * 首先检查我们是否未能从当前源读取，如果是，则推进状态机。读取失败可能发生在此函数之外，例如，当记录上的CRC检查失败时，或者在此循环内。
		 */
		if (lastSourceFailed)
		{
			/*
			 * 不允许在非阻塞的预读期间发生任何重试循环。让调用者先处理已经解码的所有内容。
			 */
			if (fc_nonblocking)
				return XLREAD_WOULDBLOCK;

			switch (currentSource)
			{
				case XLOG_FROM_ARCHIVE:
				case XLOG_FROM_PG_WAL:

					/*
					 * 检查触发文件是否存在。请注意，我们仅在失败后执行此操作，因此当您创建触发文件时，我们仍将尽可能从归档和pg_wal中完成重放，直到故障转移。
					 */
					if (StandbyMode && fc_CheckForStandbyTrigger())
					{
						XLogShutdownWalRcv();
						return XLREAD_FAIL;
					}

					/*
					 * 不在备用模式中，我们现在已经尝试了归档和pg_wal。
					 */
					if (!StandbyMode)
						return XLREAD_FAIL;

					/*
					 * 移动到XLOG_FROM_STREAM状态，并在必要时设置开始一个walreceiver。
					 */
					currentSource = XLOG_FROM_STREAM;
					fc_startWalReceiver = true;
					break;

				case XLOG_FROM_STREAM:

					/*
					 * 流式传输时失败。最有可能的是，由于流复制被终止或触发了提升，我们来到这里。但是如果我们发现从主节点流式传输的WAL中有无效记录，我们也会来到这里，那样的话就严重出问题了。问题几乎不可能自行消失，但PANIC对可用性也不好，尤其是在热备用模式下。因此，我们将其视为断开连接，并再次从归档/pg_wal重试。归档中的WAL应与流式传输的一致，因此不太可能有所帮助，但可以抱有希望......
					 */

					/*
					 * 我们应该只在备用模式中移动到XLOG_FROM_STREAM。
					 */
					Assert(StandbyMode);

					/*
					 * 在离开XLOG_FROM_STREAM状态之前，确保walreceiver未处于活动状态，以免覆盖我们从归档中恢复的WAL。
					 */
					XLogShutdownWalRcv();

					/*
					 * 在我们睡觉之前，如果请求我们恢复到最新时间线，请重新扫描可能的新时间线。
					 */
					if (recoveryTargetTimeLineGoal == RECOVERY_TARGET_TIMELINE_LATEST)
					{
						if (fc_rescanLatestTimeLine(fc_replayTLI, fc_replayLSN))
						{
							currentSource = XLOG_FROM_ARCHIVE;
							break;
						}
					}

					/*
					 * XLOG_FROM_STREAM是我们状态机中的最后一个状态，因此我们已耗尽获取请求的WAL的所有选项。我们将回到循环并从归档重试，但如果距离上次尝试时间不长，则睡眠wal_retrieve_retry_interval毫秒以避免忙等待。
					 */
					fc_now = GetCurrentTimestamp();
					if (!TimestampDifferenceExceeds(fc_last_fail_time, fc_now,
													wal_retrieve_retry_interval))
					{
						long		fc_wait_time;

						fc_wait_time = wal_retrieve_retry_interval -
							TimestampDifferenceMilliseconds(fc_last_fail_time, fc_now);

						elog(LOG, "waiting for WAL to become available at %X/%X",
							 LSN_FORMAT_ARGS(fc_RecPtr));

						/* 执行可能对我们以后有利的后台任务。 */
						KnownAssignedTransactionIdsIdleMaintenance();

						(void) WaitLatch(&XLogRecoveryCtl->recoveryWakeupLatch,
										 WL_LATCH_SET | WL_TIMEOUT |
										 WL_EXIT_ON_PM_DEATH,
										 fc_wait_time,
										 WAIT_EVENT_RECOVERY_RETRIEVE_RETRY_INTERVAL);
						ResetLatch(&XLogRecoveryCtl->recoveryWakeupLatch);
						fc_now = GetCurrentTimestamp();

						/* 处理启动进程的中断信号 */
						HandleStartupProcInterrupts();
					}
					fc_last_fail_time = fc_now;
					currentSource = XLOG_FROM_ARCHIVE;
					break;

				default:
					elog(ERROR, "unexpected WAL source %d", currentSource);
			}
		}
		else if (currentSource == XLOG_FROM_PG_WAL)
		{
			/*
			 * 我们刚刚成功读取了pg_wal中的一个文件。我们更倾向于使用归档中的文件，而不是pg_wal中的文件，因此首先尝试从归档中再次读取下一个文件。
			 */
			if (InArchiveRecovery)
				currentSource = XLOG_FROM_ARCHIVE;
		}

		if (currentSource != fc_oldSource)
			elog(DEBUG2, "switched WAL source from %s to %s after %s",
				 xlogSourceNames[fc_oldSource], xlogSourceNames[currentSource],
				 lastSourceFailed ? "failure" : "success");

		/*
		 * 我们现在已经处理了可能的失败。尝试从选定的源读取。
		 */
		lastSourceFailed = false;

		switch (currentSource)
		{
			case XLOG_FROM_ARCHIVE:
			case XLOG_FROM_PG_WAL:

				/*
				 * 在从归档或pg_wal读取WAL时，WAL接收器不能在运行状态。
				 */
				Assert(!WalRcvStreaming());

				/* 关闭我们可能打开的任何旧文件。 */
				if (readFile >= 0)
				{
					close(readFile);
					readFile = -1;
				}
				/* 如果是随机获取，则重置curFileTLI。 */
				if (fc_randAccess)
					curFileTLI = 0;

				/*
				 * 尝试从归档恢复文件，或从pg_wal读取现有文件。
				 */
				readFile = fc_XLogFileReadAnyTLI(readSegNo, DEBUG2,
											  currentSource == XLOG_FROM_ARCHIVE ? XLOG_FROM_ANY :
											  currentSource);
				if (readFile >= 0)
					return XLREAD_SUCCESS;	/* 成功！ */

				/*
				 * 不，不在归档或pg_wal中找到。
				 */
				lastSourceFailed = true;
				break;

			case XLOG_FROM_STREAM:
				{
					bool		fc_havedata;

					/*
					 * 我们应该只在备用模式中移动到XLOG_FROM_STREAM。
					 */
					Assert(StandbyMode);

					/*
					 * 首先，如果请求重启 walreceiver，则关闭它——但如果我们已经计划启动它，则没有必要。
					 */
					if (pendingWalRcvRestart && !fc_startWalReceiver)
					{
						XLogShutdownWalRcv();

						/*
						 * 如果我们请求恢复到最新的时间线，则重新扫描可能的新时间线。
						 */
						if (recoveryTargetTimeLineGoal ==
							RECOVERY_TARGET_TIMELINE_LATEST)
							fc_rescanLatestTimeLine(fc_replayTLI, fc_replayLSN);

						fc_startWalReceiver = true;
					}
					pendingWalRcvRestart = false;

					/*
					 * 如果需要，启动 walreceiver。
					 *
					 * 如果 fetching_ckpt 为真，RecPtr 指向初始
					 * 检查点位置。在这种情况下，我们使用 RedoStartLSN
					 * 作为流式起始位置，而不是 RecPtr，以便
					 * 在稍后跳回到 RedoStartLSN 开始重做时，日志已经流式传输。
					 */
					if (fc_startWalReceiver &&
						PrimaryConnInfo && strcmp(PrimaryConnInfo, "") != 0)
					{
						XLogRecPtr	fc_ptr;
						TimeLineID	fc_tli;

						if (fc_fetching_ckpt)
						{
							fc_ptr = RedoStartLSN;
							fc_tli = RedoStartTLI;
						}
						else
						{
							fc_ptr = fc_RecPtr;

							/*
							 * 使用记录开始位置来确定 TLI，而不是我们正在读取的位置。
							 */
							fc_tli = tliOfPointInHistory(fc_tliRecPtr, expectedTLEs);

							if (curFileTLI > 0 && fc_tli < curFileTLI)
								elog(ERROR, "according to history file, WAL location %X/%X belongs to timeline %u, but previous recovered WAL file came from timeline %u",
									 LSN_FORMAT_ARGS(fc_tliRecPtr),
									 fc_tli, curFileTLI);
						}
						curFileTLI = fc_tli;
						SetInstallXLogFileSegmentActive();
						RequestXLogStreaming(fc_tli, fc_ptr, PrimaryConnInfo,
											 PrimarySlotName,
											 wal_receiver_create_temp_slot);
						flushedUpto = 0;
					}

					/*
					 * 检查 WAL 接收器是否处于活动状态或等待启动。
					 */
					if (!WalRcvStreaming())
					{
						lastSourceFailed = true;
						break;
					}

					/*
					 * walreceiver 正在活动中，所以查看是否有新数据到达。
					 *
					 * 我们仅在从 walreceiver 获得新鲜的 WAL 并观察到
					 * 我们已经处理过最最近刷写到磁盘的“块”之前的所有内容时，
					 * 才会更新 XLogReceiptTime。在稳定状态下，我们与
					 * 进入的数据保持同步时，XLogReceiptTime 将在每个周期
					 * 更新。当我们落后时，XLogReceiptTime 将不会前进，
					 * 因此分配给冲突查询的宽限时间将减少。
					 */
					if (fc_RecPtr < flushedUpto)
						fc_havedata = true;
					else
					{
						XLogRecPtr	fc_latestChunkStart;

						flushedUpto = GetWalRcvFlushRecPtr(&fc_latestChunkStart, &receiveTLI);
						if (fc_RecPtr < flushedUpto && receiveTLI == curFileTLI)
						{
							fc_havedata = true;
							if (fc_latestChunkStart <= fc_RecPtr)
							{
								XLogReceiptTime = GetCurrentTimestamp();
								fc_SetCurrentChunkStartTime(XLogReceiptTime);
							}
						}
						else
							fc_havedata = false;
					}
					if (fc_havedata)
					{
						/*
						 * 太好了，已经流式传输得足够远。 如果文件尚未打开，则打开文件。
						 * 如果我们尚未初始化时间线历史记录，请读取时间线历史记录
						 * 文件；它现在应该已经流式传输了并出现在
						 * pg_wal 中。使用 XLOG_FROM_STREAM，以确保源
						 * 信息设置正确，并且 XLogReceiptTime 不会
						 * 发生变化。
						 *
						 * 注意：我们必须根据 recoveryTargetTLI 设置 readTimeLineHistory，而不是 receiveTLI。
						 * 通常它们是相同的，但如果 recovery_target_timeline 为
						 * 'latest' 并且已配置归档，则有可能我们能够从
						 * 归档中检索到一个或多个新的时间线历史文件，
						 * 更新 recoveryTargetTLI。
						 */
						if (readFile < 0)
						{
							if (!expectedTLEs)
								expectedTLEs = readTimeLineHistory(recoveryTargetTLI);
							readFile = fc_XLogFileRead(readSegNo, PANIC,
													receiveTLI,
													XLOG_FROM_STREAM, false);
							Assert(readFile >= 0);
						}
						else
						{
							/* 只需确保源信息正确... */
							readSource = XLOG_FROM_STREAM;
							XLogReceiptSource = XLOG_FROM_STREAM;
							return XLREAD_SUCCESS;
						}
						break;
					}

					/* 在非阻塞模式下，返回而不是睡眠。 */
					if (fc_nonblocking)
						return XLREAD_WOULDBLOCK;

					/*
					 * 数据尚未到达。 检查触发器，然后等待
					 * walreceiver 在新 WAL 到达时唤醒我们。
					 */
					if (fc_CheckForStandbyTrigger())
					{
						/*
						 * 请注意，我们不会立即返回 XLREAD_FAIL
						 * 在这里。在被触发后，我们仍然想重放
						 * 已经流式传输的所有 WAL。它现在在 pg_wal 中，
						 * 所以我们仅将此视为失败，
						 * 状态机将继续重放 pg_wal 中流式传输的 WAL，然后重新检查
						 * 触发器并退出重放。
						 */
						lastSourceFailed = true;
						break;
					}

					/*
					 * 由于我们已经重新播放了到目前为止收到的所有内容，并且即将开始等待更多的WAL，让我们现在告诉上游服务器我们的重放位置，以便pg_stat_replication不会显示过时的信息。
					 */
					if (!fc_streaming_reply_sent)
					{
						WalRcvForceReply();
						fc_streaming_reply_sent = true;
					}

					/* 执行任何可能对我们以后的操作有益的后台任务。 */
					KnownAssignedTransactionIdsIdleMaintenance();

					/* 在休眠之前更新pg_stat_recovery_prefetch。 */
					XLogPrefetcherComputeStats(xlogprefetcher);

					/*
					 * 等待更多的WAL到达。超时为5秒，以便及时响应触发文件并检查WAL接收器是否仍然处于活动状态。
					 */
					(void) WaitLatch(&XLogRecoveryCtl->recoveryWakeupLatch,
									 WL_LATCH_SET | WL_TIMEOUT |
									 WL_EXIT_ON_PM_DEATH,
									 5000L, WAIT_EVENT_RECOVERY_WAL_STREAM);
					ResetLatch(&XLogRecoveryCtl->recoveryWakeupLatch);
					break;
				}

			default:
				elog(ERROR, "unexpected WAL source %d", currentSource);
		}

		/*
		 * 在这里检查恢复暂停，以便我们可以更快确认请求的暂停是否已经生效。
		 */
		if (((volatile XLogRecoveryCtlData *) XLogRecoveryCtl)->recoveryPauseState !=
			RECOVERY_NOT_PAUSED)
			fc_recoveryPausesHere(false);

		/*
		 * 这个可能很长的循环需要处理启动过程的中断。
		 */
		HandleStartupProcInterrupts();
	}

	return XLREAD_FAIL;			/* 未到达 */
}


/*
 * 确定在当前WAL页面中报告一个损坏的WAL记录时应该使用什么日志级别，该页面之前由XLogPageRead()读取。
 *
 * 'emode'是用于报告文件未找到或合法的WAL结束情况的错误模式。一般来说，我们会像往常一样使用它，但如果我们正在重试之前尝试过的确切记录，只在第一次时抱怨，以减少噪音。然而，我们只在从pg_wal读取时这样做，因为我们不期望在归档或从主节点流式传输的记录中出现任何无效记录。归档中的文件应该是完整的，我们不应该碰到WAL的结束，因为我们会在重新播放之前停止并等待更多WAL到达。
 *
 * 注意：此函数记住上次调用时的RecPtr值，以抑制关于相同记录的重复消息。只有在你即将ereport()时才能调用此函数，否则可能会导致后续消息错误地被抑制。
 */
static int fc_emode_for_corrupt_record(int fc_emode, XLogRecPtr fc_RecPtr)
{
	static XLogRecPtr fc_lastComplaint = 0;

	if (readSource == XLOG_FROM_PG_WAL && fc_emode == LOG)
	{
		if (fc_RecPtr == fc_lastComplaint)
			fc_emode = DEBUG1;
		else
			fc_lastComplaint = fc_RecPtr;
	}
	return fc_emode;
}


/*
 * 子程序尝试获取和验证先前检查点记录。
 *
 * whichChkpt用于识别检查点（仅用于报告目的）。
 * 1表示“主”，0表示“其他”（backup_label）
 */
static XLogRecord * fc_ReadCheckpointRecord(XLogPrefetcher *fc_xlogprefetcher, XLogRecPtr fc_RecPtr,
					 int fc_whichChkpt, bool fc_report, TimeLineID fc_replayTLI)
{
	XLogRecord *fc_record;
	uint8		fc_info;

	Assert(xlogreader != NULL);

	if (!XRecOffIsValid(fc_RecPtr))
	{
		if (!fc_report)
			return NULL;

		switch (fc_whichChkpt)
		{
			case 1:
				ereport(LOG,
						(errmsg("invalid primary checkpoint link in control file")));
				break;
			default:
				ereport(LOG,
						(errmsg("invalid checkpoint link in backup_label file")));
				break;
		}
		return NULL;
	}

	XLogPrefetcherBeginRead(fc_xlogprefetcher, fc_RecPtr);
	fc_record = fc_ReadRecord(fc_xlogprefetcher, LOG, true, fc_replayTLI);

	if (fc_record == NULL)
	{
		if (!fc_report)
			return NULL;

		switch (fc_whichChkpt)
		{
			case 1:
				ereport(LOG,
						(errmsg("invalid primary checkpoint record")));
				break;
			default:
				ereport(LOG,
						(errmsg("invalid checkpoint record")));
				break;
		}
		return NULL;
	}
	if (fc_record->xl_rmid != RM_XLOG_ID)
	{
		switch (fc_whichChkpt)
		{
			case 1:
				ereport(LOG,
						(errmsg("invalid resource manager ID in primary checkpoint record")));
				break;
			default:
				ereport(LOG,
						(errmsg("invalid resource manager ID in checkpoint record")));
				break;
		}
		return NULL;
	}
	fc_info = fc_record->xl_info & ~XLR_INFO_MASK;
	if (fc_info != XLOG_CHECKPOINT_SHUTDOWN &&
		fc_info != XLOG_CHECKPOINT_ONLINE)
	{
		switch (fc_whichChkpt)
		{
			case 1:
				ereport(LOG,
						(errmsg("invalid xl_info in primary checkpoint record")));
				break;
			default:
				ereport(LOG,
						(errmsg("invalid xl_info in checkpoint record")));
				break;
		}
		return NULL;
	}
	if (fc_record->xl_tot_len != SizeOfXLogRecord + SizeOfXLogRecordDataHeaderShort + sizeof(CheckPoint))
	{
		switch (fc_whichChkpt)
		{
			case 1:
				ereport(LOG,
						(errmsg("invalid length of primary checkpoint record")));
				break;
			default:
				ereport(LOG,
						(errmsg("invalid length of checkpoint record")));
				break;
		}
		return NULL;
	}
	return fc_record;
}

/*
 * 扫描自我们开始恢复以来可能已出现在归档中的新时间线。
 *
 * 如果有任何新时间线，该函数将恢复目标TLI更改为最新的时间线并返回'true'。
 */
static bool fc_rescanLatestTimeLine(TimeLineID fc_replayTLI, XLogRecPtr fc_replayLSN)
{
	List	   *fc_newExpectedTLEs;
	bool		fc_found;
	ListCell   *fc_cell;
	TimeLineID	fc_newtarget;
	TimeLineID	fc_oldtarget = recoveryTargetTLI;
	TimeLineHistoryEntry *fc_currentTle = NULL;

	fc_newtarget = findNewestTimeLine(recoveryTargetTLI);
	if (fc_newtarget == recoveryTargetTLI)
	{
		/* 未找到新时间线 */
		return false;
	}

	/*
	 * 确定新TLI的预期TLI列表
	 */

	fc_newExpectedTLEs = readTimeLineHistory(fc_newtarget);

	/*
	 * 如果当前时间线不是新时间线历史的一部分，我们无法继续。
	 */
	fc_found = false;
	foreach(fc_cell, fc_newExpectedTLEs)
	{
		fc_currentTle = (TimeLineHistoryEntry *) lfirst(fc_cell);

		if (fc_currentTle->tli == recoveryTargetTLI)
		{
			fc_found = true;
			break;
		}
	}
	if (!fc_found)
	{
		ereport(LOG,
				(errmsg("new timeline %u is not a child of database system timeline %u",
						fc_newtarget,
						fc_replayTLI)));
		return false;
	}

	/*
	 * 当前时间线在历史文件中被找到，但检查下一个时间线是否在当前恢复位置*之后*从中派生。
	 */
	if (fc_currentTle->end < fc_replayLSN)
	{
		ereport(LOG,
				(errmsg("new timeline %u forked off current database system timeline %u before current recovery point %X/%X",
						fc_newtarget,
						fc_replayTLI,
						LSN_FORMAT_ARGS(fc_replayLSN))));
		return false;
	}

	/* 新时间线历史似乎有效。切换目标 */
	recoveryTargetTLI = fc_newtarget;
	list_free_deep(expectedTLEs);
	expectedTLEs = fc_newExpectedTLEs;

	/*
	 * 在StartupXLOG()中，尝试确保我们在pg_wal中拥有旧目标和新目标之间的所有历史文件。
	 */
	restoreTimeLineHistoryFiles(fc_oldtarget + 1, fc_newtarget);

	ereport(LOG,
			(errmsg("new target timeline is %u",
					recoveryTargetTLI)));

	return true;
}


/*
 * 打开一个日志文件段以供读取（在恢复期间）。
 *
 * 如果 source == XLOG_FROM_ARCHIVE，则该段是从归档中检索的。
 * 否则，假定它已经在 pg_wal 中可用。
 */
static int fc_XLogFileRead(XLogSegNo fc_segno, int fc_emode, TimeLineID fc_tli,
			 XLogSource fc_source, bool fc_notfoundOk)
{
	char		fc_xlogfname[MAXFNAMELEN];
	char		fc_activitymsg[MAXFNAMELEN + 16];
	char		fc_path[MAXPGPATH];
	int			fc_fd;

	XLogFileName(fc_xlogfname, fc_tli, fc_segno, wal_segment_size);

	switch (fc_source)
	{
		case XLOG_FROM_ARCHIVE:
			/* 在 PS 显示中报告恢复进度 */
			snprintf(fc_activitymsg, sizeof(fc_activitymsg), "waiting for %s",
					 fc_xlogfname);
			set_ps_display(fc_activitymsg);

			if (!RestoreArchivedFile(fc_path, fc_xlogfname,
									 "RECOVERYXLOG",
									 wal_segment_size,
									 InRedo))
				return -1;
			break;

		case XLOG_FROM_PG_WAL:
		case XLOG_FROM_STREAM:
			XLogFilePath(fc_path, fc_tli, fc_segno, wal_segment_size);
			break;

		default:
			elog(ERROR, "invalid XLogFileRead source %d", fc_source);
	}

	/*
	 * 如果该段是从归档存储中提取的，则用归档版本替换现有
	 * xlog 段（如果有的话）。
	 */
	if (fc_source == XLOG_FROM_ARCHIVE)
	{
		Assert(!IsInstallXLogFileSegmentActive());
		KeepFileRestoredFromArchive(fc_path, fc_xlogfname);

		/*
		 * 设置路径以指向 pg_wal 中的新文件。
		 */
		snprintf(fc_path, MAXPGPATH, XLOGDIR "/%s", fc_xlogfname);
	}

	fc_fd = BasicOpenFile(fc_path, O_RDONLY | PG_BINARY);
	if (fc_fd >= 0)
	{
		/* 成功！ */
		curFileTLI = fc_tli;

		/* 在 PS 显示中报告恢复进度 */
		snprintf(fc_activitymsg, sizeof(fc_activitymsg), "recovering %s",
				 fc_xlogfname);
		set_ps_display(fc_activitymsg);

		/* 在各种状态变量中跟踪数据源 */
		readSource = fc_source;
		XLogReceiptSource = fc_source;
		/* 在 FROM_STREAM 情况下，调用者跟踪接收时间，不是我 */
		if (fc_source != XLOG_FROM_STREAM)
			XLogReceiptTime = GetCurrentTimestamp();

		return fc_fd;
	}
	if (errno != ENOENT || !fc_notfoundOk) /* 意外故障？ */
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m", fc_path)));
	return -1;
}

/*
 * 打开一个日志文件段以供读取（在恢复期间）。
 *
 * 此版本搜索在 expectedTLEs 中列出的任何 TLI 的段。
 */
static int fc_XLogFileReadAnyTLI(XLogSegNo fc_segno, int fc_emode, XLogSource fc_source)
{
	char		fc_path[MAXPGPATH];
	ListCell   *fc_cell;
	int			fc_fd;
	List	   *fc_tles;

	/*
	 * 循环查找合适的时间线 ID：我们可能需要读取任何的
	 * 在 expectedTLEs 中列出的时间线。
	 *
	 * 我们期望在进入时 curFileTLI 是序列中前一文件的 TLI，
	 * 或者在没有前驱的情况下为 0。我们不允许 curFileTLI
	 * 向后移动；这可以防止我们在父时间线扩展到比子时间线
	 * 更高的段号时选取错误的文件。
	 *
	 * 如果我们还没有读取时间线历史文件，现在就读取它，
	 * 以便我们知道要扫描哪些 TLI。不过，除非我们找到
	 * 有效的段，否则不会将列表保存在 expectedTLEs 中。
	 * 这样，如果既没有时间线历史文件也没有 WAL 段在归档中，
	 * 而且设置了流复制，当我们开始流复制时，会从
	 * 主服务器读取流的时间线历史文件，而不是用
	 * 这里生成的虚拟历史进行恢复。
	 */
	if (expectedTLEs)
		fc_tles = expectedTLEs;
	else
		fc_tles = readTimeLineHistory(recoveryTargetTLI);

	foreach(fc_cell, fc_tles)
	{
		TimeLineHistoryEntry *fc_hent = (TimeLineHistoryEntry *) lfirst(fc_cell);
		TimeLineID	fc_tli = fc_hent->tli;

		if (fc_tli < curFileTLI)
			break;				/* 不用担心查看过老的 TLI */

		/*
		 * 跳过扫描不属于要读取的日志文件段的时间线 ID
		 */
		if (fc_hent->begin != InvalidXLogRecPtr)
		{
			XLogSegNo	fc_beginseg = 0;

			XLByteToSeg(fc_hent->begin, fc_beginseg, wal_segment_size);

			/*
			 * 不属于时间线的日志文件段
			 * 比时间线开始或结束的段分别要旧或要新。这里只需检查时间线的
			 * 起始段。由于时间线在这个循环中以降序扫描，任何比结束段更新的
			 * 段应该属于更新的时间线并且已经被读取过。因此这里不需要检查
			 * 时间线的结束段。
			 */
			if (fc_segno < fc_beginseg)
				continue;
		}

		if (fc_source == XLOG_FROM_ANY || fc_source == XLOG_FROM_ARCHIVE)
		{
			fc_fd = fc_XLogFileRead(fc_segno, fc_emode, fc_tli,
							  XLOG_FROM_ARCHIVE, true);
			if (fc_fd != -1)
			{
				elog(DEBUG1, "got WAL segment from archive");
				if (!expectedTLEs)
					expectedTLEs = fc_tles;
				return fc_fd;
			}
		}

		if (fc_source == XLOG_FROM_ANY || fc_source == XLOG_FROM_PG_WAL)
		{
			fc_fd = fc_XLogFileRead(fc_segno, fc_emode, fc_tli,
							  XLOG_FROM_PG_WAL, true);
			if (fc_fd != -1)
			{
				if (!expectedTLEs)
					expectedTLEs = fc_tles;
				return fc_fd;
			}
		}
	}

	/* 找不到它。为了简单起见，抱怨前时间线 */
	XLogFilePath(fc_path, recoveryTargetTLI, fc_segno, wal_segment_size);
	errno = ENOENT;
	ereport(fc_emode,
			(errcode_for_file_access(),
			 errmsg("could not open file \"%s\": %m", fc_path)));
	return -1;
}

/*
 * 设置标志以通知 walreceiver 重新启动。（启动过程在注意到相关配置更改时调用
 * 这个。）
 */
void StartupRequestWalReceiverRestart(void)
{
	if (currentSource == XLOG_FROM_STREAM && WalRcvRunning())
	{
		ereport(LOG,
				(errmsg("WAL receiver process shutdown requested")));

		pendingWalRcvRestart = true;
	}
}


/*
 * 是否已触发备用提升？
 *
 * 与 CheckForStandbyTrigger() 不同，这在任何
 * 连接到共享内存的进程中都有效。
 */
bool PromoteIsTriggered(void)
{
	/*
	 * 我们每次检查共享状态，直到触发备用提升。我们不能再次触发提升，因此在共享变量首次被看到为真之后，不需要继续检查。
	 */
	if (LocalPromoteIsTriggered)
		return true;

	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	LocalPromoteIsTriggered = XLogRecoveryCtl->SharedPromoteIsTriggered;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	return LocalPromoteIsTriggered;
}

static void fc_SetPromoteIsTriggered(void)
{
	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	XLogRecoveryCtl->SharedPromoteIsTriggered = true;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	/*
	 * 将恢复暂停状态标记为“未暂停”，因为如果在恢复暂停期间触发提升，则暂停状态结束，提升继续进行。否则，pg_get_wal_replay_pause_state()可能会在提升进行时错误地返回“暂停”。
	 */
	SetRecoveryPause(false);

	LocalPromoteIsTriggered = true;
}

/*
 * 检查用户指定的触发文件是否存在，以及是否接收到提升请求。如果任一条件成立，返回真。
 */
static bool fc_CheckForStandbyTrigger(void)
{
	struct stat fc_stat_buf;

	if (LocalPromoteIsTriggered)
		return true;

	if (IsPromoteSignaled() && CheckPromoteSignal())
	{
		ereport(LOG, (errmsg("received promote request")));
		RemovePromoteSignalFiles();
		ResetPromoteSignaled();
		fc_SetPromoteIsTriggered();
		return true;
	}

	if (PromoteTriggerFile == NULL || strcmp(PromoteTriggerFile, "") == 0)
		return false;

	if (stat(PromoteTriggerFile, &fc_stat_buf) == 0)
	{
		ereport(LOG,
				(errmsg("promote trigger file found: %s", PromoteTriggerFile)));
		unlink(PromoteTriggerFile);
		fc_SetPromoteIsTriggered();
		return true;
	}
	else if (errno != ENOENT)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not stat promote trigger file \"%s\": %m",
						PromoteTriggerFile)));

	return false;
}

/*
 * 删除信号备用提升请求的文件。
 */
void RemovePromoteSignalFiles(void)
{
	unlink(PROMOTE_SIGNAL_FILE);
}

/*
 * 检查是否已到达提升请求。
 */
bool CheckPromoteSignal(void)
{
	struct stat fc_stat_buf;

	if (stat(PROMOTE_SIGNAL_FILE, &fc_stat_buf) == 0)
		return true;

	return false;
}

/*
 * 唤醒启动进程以重放新到达的WAL，或通知请求故障转移。
 */
void WakeupRecovery(void)
{
	SetLatch(&XLogRecoveryCtl->recoveryWakeupLatch);
}

/*
 * 在主要恢复循环中安排walreceiver唤醒。
 */
void XLogRequestWalReceiverReply(void)
{
	doRequestWalReceiverReply = true;
}

/*
 * 热备份是否已经激活？这在特殊的后端中很重要，因为在返回真之前，正常外部后端永远无法连接。Postmaster通过信号知道这一点，而不是通过共享内存。
 *
 * 与测试standbyState不同，这在任何与共享内存连接的进程中都有效。（并且请注意，standbyState本身并不能传达真实情况。）
 */
bool HotStandbyActive(void)
{
	/*
	 * 我们每次检查共享状态，直到热备份激活。我们不能停用热备份，因此在共享变量首次被看到为真之后，不需要继续检查。
	 */
	if (LocalHotStandbyActive)
		return true;
	else
	{
		/* 自旋锁在内存顺序较弱的机器上至关重要！ */
		SpinLockAcquire(&XLogRecoveryCtl->info_lck);
		LocalHotStandbyActive = XLogRecoveryCtl->SharedHotStandbyActive;
		SpinLockRelease(&XLogRecoveryCtl->info_lck);

		return LocalHotStandbyActive;
	}
}

/*
 * 类似于HotStandbyActive()，但仅在WAL重放代码中使用，在这里我们不需要询问任何其他进程状态是什么。
 */
static bool fc_HotStandbyActiveInReplay(void)
{
	Assert(AmStartupProcess() || !IsPostmasterEnvironment);
	return LocalHotStandbyActive;
}

/*
 * 获取最新的重做应用位置。
 *
 * 导出以允许WALReceiver直接读取指针。
 */
XLogRecPtr GetXLogReplayRecPtr(TimeLineID *fc_replayTLI)
{
	XLogRecPtr	fc_recptr;
	TimeLineID	fc_tli;

	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	fc_recptr = XLogRecoveryCtl->lastReplayedEndRecPtr;
	fc_tli = XLogRecoveryCtl->lastReplayedTLI;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	if (fc_replayTLI)
		*fc_replayTLI = fc_tli;
	return fc_recptr;
}


/*
 * 获取最后应用的记录位置，或正在被应用的记录位置。
 *
 * 这与GetXLogReplayRecPtr()不同，因为如果当前正在应用WAL记录，则包括该记录。
 */
XLogRecPtr GetCurrentReplayRecPtr(TimeLineID *fc_replayEndTLI)
{
	XLogRecPtr	fc_recptr;
	TimeLineID	fc_tli;

	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	fc_recptr = XLogRecoveryCtl->replayEndRecPtr;
	fc_tli = XLogRecoveryCtl->replayEndTLI;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	if (fc_replayEndTLI)
		*fc_replayEndTLI = fc_tli;
	return fc_recptr;
}

/*
 * 保存最新处理的提交/中止记录的时间戳。
 *
 * 我们将其保存在XLogRecoveryCtl中，而不是简单的静态变量中，以便其他进程可以看到，特别注意的是CreateRestartPoint在检查点过程中执行。
 */
static void fc_SetLatestXTime(TimestampTz fc_xtime)
{
	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	XLogRecoveryCtl->recoveryLastXTime = fc_xtime;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);
}

/*
 * 获取最新处理的提交/中止记录的时间戳。
 */
TimestampTz GetLatestXTime(void)
{
	TimestampTz fc_xtime;

	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	fc_xtime = XLogRecoveryCtl->recoveryLastXTime;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	return fc_xtime;
}

/*
 * 保存下一块WAL记录要应用的时间戳。
 *
 * 我们将其保存在XLogRecoveryCtl中，而不是简单的静态变量中，以便所有后端都可以看到。
 */
static void fc_SetCurrentChunkStartTime(TimestampTz fc_xtime)
{
	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	XLogRecoveryCtl->currentChunkStartTime = fc_xtime;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);
}

/*
 * 获取最新处理的提交/中止记录的时间戳。
 * 启动进程在XLogReceiptTime中维护准确的本地副本。
 */
TimestampTz GetCurrentChunkReplayStartTime(void)
{
	TimestampTz fc_xtime;

	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
	fc_xtime = XLogRecoveryCtl->currentChunkStartTime;
	SpinLockRelease(&XLogRecoveryCtl->info_lck);

	return fc_xtime;
}

/*
 * 返回当前块XLOG数据的接收时间，以及它是从流复制还是从归档接收的。
 */
void GetXLogReceiptTime(TimestampTz *fc_rtime, bool *fc_fromStream)
{
	/*
	 * 这必须在启动过程中执行，因为我们不将相关状态导出到共享内存中。
	 */
	Assert(InRecovery);

	*fc_rtime = XLogReceiptTime;
	*fc_fromStream = (XLogReceiptSource == XLOG_FROM_STREAM);
}

/*
 * 请注意，提供的文本字段是一个参数名称，不需要翻译
 */
void RecoveryRequiresIntParameter(const char *fc_param_name, int fc_currValue, int fc_minValue)
{
	if (fc_currValue < fc_minValue)
	{
		if (fc_HotStandbyActiveInReplay())
		{
			bool		fc_warned_for_promote = false;

			ereport(WARNING,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("hot standby is not possible because of insufficient parameter settings"),
					 errdetail("%s = %d is a lower setting than on the primary server, where its value was %d.",
							   fc_param_name,
							   fc_currValue,
							   fc_minValue)));

			SetRecoveryPause(true);

			ereport(LOG,
					(errmsg("recovery has paused"),
					 errdetail("If recovery is unpaused, the server will shut down."),
					 errhint("You can then restart the server after making the necessary configuration changes.")));

			while (GetRecoveryPauseState() != RECOVERY_NOT_PAUSED)
			{
				HandleStartupProcInterrupts();

				if (fc_CheckForStandbyTrigger())
				{
					if (!fc_warned_for_promote)
						ereport(WARNING,
								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
								 errmsg("promotion is not possible because of insufficient parameter settings"),

						/*
						 * 重复上述细节，以便在日志中易于查找。
						 */
								 errdetail("%s = %d is a lower setting than on the primary server, where its value was %d.",
										   fc_param_name,
										   fc_currValue,
										   fc_minValue),
								 errhint("Restart the server after making the necessary configuration changes.")));
					fc_warned_for_promote = true;
				}

				/*
				 * 如果请求恢复暂停，则将其设置为暂停。在循环中，
				 * 用户可能会恢复并再次暂停，因此每次都设置此项。
				 */
				fc_ConfirmRecoveryPaused();

				/*
				 * 我们在一个条件变量上等待，该变量将在暂停结束时唤醒我们，
				 * 但我们使用超时，以便我们也可以定期检查上述条件。
				 */
				ConditionVariableTimedSleep(&XLogRecoveryCtl->recoveryNotPausedCV, 1000,
											WAIT_EVENT_RECOVERY_PAUSE);
			}
			ConditionVariableCancelSleep();
		}

		ereport(FATAL,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("recovery aborted because of insufficient parameter settings"),
		/* 重复上述细节，以便在日志中易于查找。 */
				 errdetail("%s = %d is a lower setting than on the primary server, where its value was %d.",
						   fc_param_name,
						   fc_currValue,
						   fc_minValue),
				 errhint("You can restart the server after making the necessary configuration changes.")));
	}
}
